diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/CabArchiver.cs b/src/Microsoft.PackageManagement.ArchiverProviders/CabArchiver.cs new file mode 100644 index 000000000..c5489cf36 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/CabArchiver.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Archivers.Internal { + using System; + using System.Collections.Generic; + using PackageManagement.Internal; + using PackageManagement.Internal.Implementation; + + public class CabArchiver { + private static readonly Dictionary _features = new Dictionary { + {Constants.Features.SupportedExtensions, new[] {"cab", "msu"}}, + {Constants.Features.MagicSignatures, new[] {Constants.Signatures.Cab}} + }; + + /// + /// Returns a collection of strings to the client advertizing features this provider supports. + /// + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void GetFeatures(Request request) { + if( request == null ) { + throw new ArgumentNullException("request"); + } + + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::GetFeatures' ", ArchiverName); + foreach (var feature in _features) { + request.Yield(feature); + } + } + + /// + /// Returns the name of the Provider. + /// + /// + public string ArchiverName { + get { return "cabfile"; } + } + + /// + /// Returns the version of the Provider. + /// + /// The version of this provider + public string ProviderVersion { + get { + return "1.0.0.0"; + } + } + + public IEnumerable UnpackArchive(string localFilename, string destinationFolder, Request request) { + return null; + } + + public bool IsSupportedFile(string localFilename) { + return false; + } + } +} + diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveException.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveException.cs new file mode 100644 index 000000000..47a6ecf29 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveException.cs @@ -0,0 +1,53 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression +{ + using System; + using System.IO; + + /// + /// Base exception class for compression operations. Compression libraries should + /// derive subclass exceptions with more specific error information relevent to the + /// file format. + /// + public class ArchiveException : IOException + { + /// + /// Creates a new ArchiveException with a specified error message and a reference to the + /// inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. If the + /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception + /// is raised in a catch block that handles the inner exception. + public ArchiveException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Creates a new ArchiveException with a specified error message. + /// + /// The message that describes the error. + public ArchiveException(string message) + : this(message, null) + { + } + + /// + /// Creates a new ArchiveException. + /// + public ArchiveException() + : this(null, null) + { + } + + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveFileInfo.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveFileInfo.cs new file mode 100644 index 000000000..94bdf14a1 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveFileInfo.cs @@ -0,0 +1,410 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.IO; +#if !CORECLR + using System.Security.Permissions; +#endif + + /// + /// Abstract object representing a compressed file within an archive; + /// provides operations for getting the file properties and unpacking + /// the file. + /// + public abstract class ArchiveFileInfo : FileSystemInfo + { + private ArchiveInfo archiveInfo; + private string name; + private string path; + + private bool initialized; + private bool exists; + private int archiveNumber; + private FileAttributes attributes; + private DateTime lastWriteTime; + private long length; + + /// + /// Creates a new ArchiveFileInfo object representing a file within + /// an archive in a specified path. + /// + /// An object representing the archive + /// containing the file. + /// The path to the file within the archive. + /// Usually, this is a simple file name, but if the archive contains + /// a directory structure this may include the directory. + protected ArchiveFileInfo(ArchiveInfo archiveInfo, string filePath) + : base() + { + if (filePath == null) + { + throw new ArgumentNullException("filePath"); + } + + this.Archive = archiveInfo; + + this.name = System.IO.Path.GetFileName(filePath); + this.path = System.IO.Path.GetDirectoryName(filePath); + + this.attributes = FileAttributes.Normal; + this.lastWriteTime = DateTime.MinValue; + } + + /// + /// Creates a new ArchiveFileInfo object with all parameters specified; + /// used by subclasses when reading the metadata out of an archive. + /// + /// The internal path and name of the file in + /// the archive. + /// The archive number where the file + /// starts. + /// The stored attributes of the file. + /// The stored last write time of the + /// file. + /// The uncompressed size of the file. + protected ArchiveFileInfo( + string filePath, + int archiveNumber, + FileAttributes attributes, + DateTime lastWriteTime, + long length) + : this(null, filePath) + { + this.exists = true; + this.archiveNumber = archiveNumber; + this.attributes = attributes; + this.lastWriteTime = lastWriteTime; + this.length = length; + this.initialized = true; + } + + /// + /// Gets the name of the file. + /// + /// The name of the file, not including any path. + public override string Name + { + get + { + return this.name; + } + } + + /// + /// Gets the internal path of the file in the archive. + /// + /// The internal path of the file in the archive, not including + /// the file name. + public string Path + { + get + { + return this.path; + } + } + + /// + /// Gets the full path to the file. + /// + /// The full path to the file, including the full path to the + /// archive, the internal path in the archive, and the file name. + /// + /// For example, the path "C:\archive.cab\file.txt" refers to + /// a file "file.txt" inside the archive "archive.cab". + /// + public override string FullName + { + get + { + string fullName = System.IO.Path.Combine(this.Path, this.Name); + + if (this.Archive != null) + { + fullName = System.IO.Path.Combine(this.ArchiveName, fullName); + } + + return fullName; + } + } + + public string FullNameExtension + { + get + { + // GetFullPathInternal would have already stripped out the terminating "." if present. + int len = FullName.Length; + for (int i = len; --i >= 0;) + { + char ch = FullName[i]; + if (ch == '.') + return FullName.Substring(i, len - i); + if (ch == System.IO.Path.DirectorySeparatorChar || ch == System.IO.Path.AltDirectorySeparatorChar || ch == System.IO.Path.VolumeSeparatorChar) + break; + } + return String.Empty; + } + } + + /// + /// Gets or sets the archive that contains this file. + /// + /// + /// The ArchiveInfo instance that retrieved this file information -- this + /// may be null if the ArchiveFileInfo object was returned directly from + /// a stream. + /// + public ArchiveInfo Archive + { + get + { + return (ArchiveInfo) this.archiveInfo; + } + + internal set + { + this.archiveInfo = value; + + // protected instance members inherited from FileSystemInfo: + this.OriginalPath = (value != null ? value.FullName : null); + this.FullPath = this.OriginalPath; + } + } + + /// + /// Gets the full path of the archive that contains this file. + /// + /// The full path of the archive that contains this file. + public string ArchiveName + { + get + { + return this.Archive != null ? this.Archive.FullName : null; + } + } + + /// + /// Gets the number of the archive where this file starts. + /// + /// The number of the archive where this file starts. + /// A single archive or the first archive in a chain is + /// numbered 0. + public int ArchiveNumber + { + get + { + return this.archiveNumber; + } + } + + /// + /// Checks if the file exists within the archive. + /// + /// True if the file exists, false otherwise. + public override bool Exists + { + get + { + if (!this.initialized) + { + this.Refresh(); + } + + return this.exists; + } + } + + /// + /// Gets the uncompressed size of the file. + /// + /// The uncompressed size of the file in bytes. + public long Length + { + get + { + if (!this.initialized) + { + this.Refresh(); + } + + return this.length; + } + } + + /// + /// Gets the attributes of the file. + /// + /// The attributes of the file as stored in the archive. + public new FileAttributes Attributes + { + get + { + if (!this.initialized) + { + this.Refresh(); + } + + return this.attributes; + } + } + + /// + /// Gets the last modification time of the file. + /// + /// The last modification time of the file as stored in the + /// archive. + public new DateTime LastWriteTime + { + get + { + if (!this.initialized) + { + this.Refresh(); + } + + return this.lastWriteTime; + } + } + + /// + /// Gets the full path to the file. + /// + /// The same as + public override string ToString() + { + return this.FullName; + } + + /// + /// Deletes the file. NOT SUPPORTED. + /// + /// Files cannot be deleted + /// from an existing archive. + public override void Delete() + { + throw new NotSupportedException(); + } + + /// + /// Refreshes the attributes and other cached information about the file, + /// by re-reading the information from the archive. + /// + public new void Refresh() + { + base.Refresh(); + + if (this.Archive != null) + { + string filePath = System.IO.Path.Combine(this.Path, this.Name); + ArchiveFileInfo updatedFile = this.Archive.GetFile(filePath); + if (updatedFile == null) + { + throw new FileNotFoundException( + "File not found in archive.", filePath); + } + + this.Refresh(updatedFile); + } + } + + /// + /// Extracts the file. + /// + /// The destination path where the file + /// will be extracted. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void CopyTo(string destFileName) + { + this.CopyTo(destFileName, false); + } + + /// + /// Extracts the file, optionally overwriting any existing file. + /// + /// The destination path where the file + /// will be extracted. + /// If true, + /// will be overwritten if it exists. + /// is false + /// and exists. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void CopyTo(string destFileName, bool overwrite) + { + if (destFileName == null) + { + throw new ArgumentNullException("destFileName"); + } + + if (!overwrite && File.Exists(destFileName)) + { + throw new IOException(); + } + + if (this.Archive == null) + { + throw new InvalidOperationException(); + } + + this.Archive.UnpackFile( + System.IO.Path.Combine(this.Path, this.Name), destFileName); + } + + /// + /// Opens the archive file for reading without actually extracting the + /// file to disk. + /// + /// + /// A stream for reading directly from the packed file. Like any stream + /// this should be closed/disposed as soon as it is no longer needed. + /// + public Stream OpenRead() + { + return this.Archive.OpenRead(System.IO.Path.Combine(this.Path, this.Name)); + } + + /// + /// Opens the archive file reading text with UTF-8 encoding without + /// actually extracting the file to disk. + /// + /// + /// A reader for reading text directly from the packed file. Like any reader + /// this should be closed/disposed as soon as it is no longer needed. + /// + /// + /// To open an archived text file with different encoding, use the + /// method and pass the returned stream to one of + /// the constructor overloads. + /// + public StreamReader OpenText() + { + return this.Archive.OpenText(System.IO.Path.Combine(this.Path, this.Name)); + } + + /// + /// Refreshes the information in this object with new data retrieved + /// from an archive. + /// + /// Fresh instance for the same file just + /// read from the archive. + /// + /// Subclasses may override this method to refresh sublcass fields. + /// However they should always call the base implementation first. + /// + protected virtual void Refresh(ArchiveFileInfo newFileInfo) + { + this.exists = newFileInfo.exists; + this.length = newFileInfo.length; + this.attributes = newFileInfo.attributes; + this.lastWriteTime = newFileInfo.lastWriteTime; + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveFileStreamContext.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveFileStreamContext.cs new file mode 100644 index 000000000..1b6588842 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveFileStreamContext.cs @@ -0,0 +1,682 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression +{ + using System; + using System.Collections.Generic; + using System.IO; + + /// + /// Provides a basic implementation of the archive pack and unpack stream context + /// interfaces, based on a list of archive files, a default directory, and an + /// optional mapping from internal to external file paths. + /// + /// + /// This class can also handle creating or extracting chained archive packages. + /// + public class ArchiveFileStreamContext + : IPackStreamContext, IUnpackStreamContext + { + private IList archiveFiles; + private string directory; + private IDictionary files; + private bool extractOnlyNewerFiles; + private bool enableOffsetOpen; + + #region Constructors + + /// + /// Creates a new ArchiveFileStreamContext with a archive file and + /// no default directory or file mapping. + /// + /// The path to a archive file that will be + /// created or extracted. + public ArchiveFileStreamContext(string archiveFile) + : this(archiveFile, null, null) + { + } + + /// + /// Creates a new ArchiveFileStreamContext with a archive file, default + /// directory and mapping from internal to external file paths. + /// + /// The path to a archive file that will be + /// created or extracted. + /// The default root directory where files will be + /// located, optional. + /// A mapping from internal file paths to external file + /// paths, optional. + /// + /// If the mapping is not null and a file is not included in the mapping, + /// the file will be skipped. + /// If the external path in the mapping is a simple file name or + /// relative file path, it will be concatenated onto the default directory, + /// if one was specified. + /// For more about how the default directory and files mapping are + /// used, see and + /// . + /// + public ArchiveFileStreamContext( + string archiveFile, + string directory, + IDictionary files) + : this(new string[] { archiveFile }, directory, files) + { + if (archiveFile == null) + { + throw new ArgumentNullException("archiveFile"); + } + } + + /// + /// Creates a new ArchiveFileStreamContext with a list of archive files, + /// a default directory and a mapping from internal to external file paths. + /// + /// A list of paths to archive files that will be + /// created or extracted. + /// The default root directory where files will be + /// located, optional. + /// A mapping from internal file paths to external file + /// paths, optional. + /// + /// When creating chained archives, the list + /// should include at least enough archives to handle the entire set of + /// input files, based on the maximum archive size that is passed to the + /// .. + /// If the mapping is not null and a file is not included in the mapping, + /// the file will be skipped. + /// If the external path in the mapping is a simple file name or + /// relative file path, it will be concatenated onto the default directory, + /// if one was specified. + /// For more about how the default directory and files mapping are used, + /// see and + /// . + /// + public ArchiveFileStreamContext( + IList archiveFiles, + string directory, + IDictionary files) + { + if (archiveFiles == null || archiveFiles.Count == 0) + { + throw new ArgumentNullException("archiveFiles"); + } + + this.archiveFiles = archiveFiles; + this.directory = directory; + this.files = files; + } + + #endregion + + #region Properties + + /// + /// Gets or sets the list of archive files that are created or extracted. + /// + /// The list of archive files that are created or extracted. + public IList ArchiveFiles + { + get + { + return this.archiveFiles; + } + } + + /// + /// Gets or sets the default root directory where files are located. + /// + /// The default root directory where files are located. + /// + /// For details about how the default directory is used, + /// see and . + /// + public string Directory + { + get + { + return this.directory; + } + } + + /// + /// Gets or sets the mapping from internal file paths to external file paths. + /// + /// A mapping from internal file paths to external file paths. + /// + /// For details about how the files mapping is used, + /// see and . + /// + public IDictionary Files + { + get + { + return this.files; + } + } + + /// + /// Gets or sets a flag that can prevent extracted files from overwriting + /// newer files that already exist. + /// + /// True to prevent overwriting newer files that already exist + /// during extraction; false to always extract from the archive regardless + /// of existing files. + public bool ExtractOnlyNewerFiles + { + get + { + return this.extractOnlyNewerFiles; + } + + set + { + this.extractOnlyNewerFiles = value; + } + } + + /// + /// Gets or sets a flag that enables creating or extracting an archive + /// at an offset within an existing file. (This is typically used to open + /// archive-based self-extracting packages.) + /// + /// True to search an existing package file for an archive offset + /// or the end of the file;/ false to always create or open a plain + /// archive file. + public bool EnableOffsetOpen + { + get + { + return this.enableOffsetOpen; + } + + set + { + this.enableOffsetOpen = value; + } + } + + #endregion + + #region IPackStreamContext Members + + /// + /// Gets the name of the archive with a specified number. + /// + /// The 0-based index of the archive within + /// the chain. + /// The name of the requested archive. May be an empty string + /// for non-chained archives, but may never be null. + /// This method returns the file name of the archive from the + /// list with the specified index, or an empty + /// string if the archive number is outside the bounds of the list. The + /// file name should not include any directory path. + public virtual string GetArchiveName(int archiveNumber) + { + if (archiveNumber < this.archiveFiles.Count) + { + return Path.GetFileName(this.archiveFiles[archiveNumber]); + } + + return String.Empty; + } + + /// + /// Opens a stream for writing an archive. + /// + /// The 0-based index of the archive within + /// the chain. + /// The name of the archive that was returned + /// by . + /// True if the stream should be truncated when + /// opened (if it already exists); false if an existing stream is being + /// re-opened for writing additional data. + /// Instance of the compression engine + /// doing the operations. + /// A writable Stream where the compressed archive bytes will be + /// written, or null to cancel the archive creation. + /// + /// This method opens the file from the list + /// with the specified index. If the archive number is outside the bounds + /// of the list, this method returns null. + /// If the flag is set, this method + /// will seek to the start of any existing archive in the file, or to the + /// end of the file if the existing file is not an archive. + /// + public virtual Stream OpenArchiveWriteStream( + int archiveNumber, + string archiveName, + bool truncate, + CompressionEngine compressionEngine) + { + if (archiveNumber >= this.archiveFiles.Count) + { + return null; + } + + if (string.IsNullOrWhiteSpace(archiveName)) + { + throw new ArgumentNullException("archiveName"); + } + + // All archives must be in the same directory, + // so always use the directory from the first archive. + string archiveFile = Path.Combine( + Path.GetDirectoryName(this.archiveFiles[0]), archiveName); + Stream stream = File.Open( + archiveFile, + (truncate ? FileMode.OpenOrCreate : FileMode.Open), + FileAccess.ReadWrite); + + if (this.enableOffsetOpen) + { + if (compressionEngine == null) { + throw new ArgumentNullException("compressionEngine"); + } + + long offset = compressionEngine.FindArchiveOffset( + new DuplicateStream(stream)); + + // If this is not an archive file, append the archive to it. + if (offset < 0) + { + offset = stream.Length; + } + + if (offset > 0) + { + stream = new OffsetStream(stream, offset); + } + + stream.Seek(0, SeekOrigin.Begin); + } + + if (truncate) + { + // Truncate the stream, in case a larger old archive starts here. + stream.SetLength(0); + } + + return stream; + } + + /// + /// Closes a stream where an archive package was written. + /// + /// The 0-based index of the archive within + /// the chain. + /// The name of the archive that was previously + /// returned by . + /// A stream that was previously returned by + /// and is now ready to be closed. + public virtual void CloseArchiveWriteStream( + int archiveNumber, + string archiveName, + Stream stream) + { + if (stream != null) + { +#if CORECLR + stream.Dispose(); +#else + stream.Close(); +#endif + + FileStream fileStream = stream as FileStream; + if (fileStream != null) + { + string streamFile = fileStream.Name; + if (!string.IsNullOrWhiteSpace(archiveName) && + archiveName != Path.GetFileName(streamFile)) + { + string archiveFile = Path.Combine( + Path.GetDirectoryName(this.archiveFiles[0]), archiveName); + if (File.Exists(archiveFile)) + { + File.Delete(archiveFile); + } + File.Move(streamFile, archiveFile); + } + } + } + } + + /// + /// Opens a stream to read a file that is to be included in an archive. + /// + /// The path of the file within the archive. + /// The returned attributes of the opened file, + /// to be stored in the archive. + /// The returned last-modified time of the + /// opened file, to be stored in the archive. + /// A readable Stream where the file bytes will be read from + /// before they are compressed, or null to skip inclusion of the file and + /// continue to the next file. + /// + /// This method opens a file using the following logic: + /// + /// If the and the mapping + /// are both null, the path is treated as relative to the current directory, + /// and that file is opened. + /// If the is not null but the + /// mapping is null, the path is treated as relative to that directory, and + /// that file is opened. + /// If the is null but the + /// mapping is not null, the path parameter is used as a key into the mapping, + /// and the resulting value is the file path that is opened, relative to the + /// current directory (or it may be an absolute path). If no mapping exists, + /// the file is skipped. + /// If both the and the + /// mapping are specified, the path parameter is used as a key into the + /// mapping, and the resulting value is the file path that is opened, relative + /// to the specified directory (or it may be an absolute path). If no mapping + /// exists, the file is skipped. + /// + /// + public virtual Stream OpenFileReadStream( + string path, out FileAttributes attributes, out DateTime lastWriteTime) + { + string filePath = this.TranslateFilePath(path); + + if (filePath == null) + { + attributes = FileAttributes.Normal; + lastWriteTime = DateTime.Now; + return null; + } + + attributes = File.GetAttributes(filePath); + lastWriteTime = File.GetLastWriteTime(filePath); + return File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + } + + /// + /// Closes a stream that has been used to read a file. + /// + /// The path of the file within the archive; the same as + /// the path provided when the stream was opened. + /// A stream that was previously returned by + /// and is now ready to be closed. + public virtual void CloseFileReadStream(string path, Stream stream) + { + if (stream != null) + { +#if CORECLR + stream.Dispose(); +#else + stream.Close(); +#endif + } + } + + /// + /// Gets extended parameter information specific to the compression format + /// being used. + /// + /// Name of the option being requested. + /// Parameters for the option; for per-file options, + /// the first parameter is typically the internal file path. + /// Option value, or null to use the default behavior. + /// + /// This implementation does not handle any options. Subclasses may override + /// this method to allow for non-default behavior. + /// + public virtual object GetOption(string optionName, object[] parameters) + { + return null; + } + + #endregion + + #region IUnpackStreamContext Members + + /// + /// Opens the archive stream for reading. + /// + /// The zero-based index of the archive to + /// open. + /// The name of the archive being opened. + /// Instance of the compression engine + /// doing the operations. + /// A stream from which archive bytes are read, or null to cancel + /// extraction of the archive. + /// + /// This method opens the file from the list with + /// the specified index. If the archive number is outside the bounds of the + /// list, this method returns null. + /// If the flag is set, this method will + /// seek to the start of any existing archive in the file, or to the end of + /// the file if the existing file is not an archive. + /// + public virtual Stream OpenArchiveReadStream( + int archiveNumber, string archiveName, CompressionEngine compressionEngine) + { + if (archiveNumber >= this.archiveFiles.Count) + { + return null; + } + + string archiveFile = this.archiveFiles[archiveNumber]; + Stream stream = File.Open( + archiveFile, FileMode.Open, FileAccess.Read, FileShare.Read); + + if (this.enableOffsetOpen) + { + if (compressionEngine == null) { + throw new ArgumentNullException("compressionEngine"); + } + long offset = compressionEngine.FindArchiveOffset( + new DuplicateStream(stream)); + if (offset > 0) + { + stream = new OffsetStream(stream, offset); + } + else + { + stream.Seek(0, SeekOrigin.Begin); + } + } + + return stream; + } + + /// + /// Closes a stream where an archive was read. + /// + /// The archive number of the stream + /// to close. + /// The name of the archive being closed. + /// The stream that was previously returned by + /// and is now ready to be closed. + public virtual void CloseArchiveReadStream( + int archiveNumber, string archiveName, Stream stream) + { + if (stream != null) + { +#if CORECLR + stream.Dispose(); +#else + stream.Close(); +#endif + } + } + + /// + /// Opens a stream for writing extracted file bytes. + /// + /// The path of the file within the archive. + /// The uncompressed size of the file to be + /// extracted. + /// The last write time of the file to be + /// extracted. + /// A stream where extracted file bytes are to be written, or null + /// to skip extraction of the file and continue to the next file. + /// + /// This method opens a file using the following logic: + /// + /// If the and the mapping + /// are both null, the path is treated as relative to the current directory, + /// and that file is opened. + /// If the is not null but the + /// mapping is null, the path is treated as relative to that directory, and + /// that file is opened. + /// If the is null but the + /// mapping is not null, the path parameter is used as a key into the mapping, + /// and the resulting value is the file path that is opened, relative to the + /// current directory (or it may be an absolute path). If no mapping exists, + /// the file is skipped. + /// If both the and the + /// mapping are specified, the path parameter is used as a key into the + /// mapping, and the resulting value is the file path that is opened, + /// relative to the specified directory (or it may be an absolute path). + /// If no mapping exists, the file is skipped. + /// + /// If the flag is set, the file + /// is skipped if a file currently exists in the same path with an equal + /// or newer write time. + /// + public virtual Stream OpenFileWriteStream( + string path, + long fileSize, + DateTime lastWriteTime) + { + string filePath = this.TranslateFilePath(path); + + if (filePath == null) + { + return null; + } + + FileInfo file = new FileInfo(filePath); + if (file.Exists) + { + if (this.extractOnlyNewerFiles && lastWriteTime != DateTime.MinValue) + { + if (file.LastWriteTime >= lastWriteTime) + { + return null; + } + } + + // Clear attributes that will prevent overwriting the file. + // (The final attributes will be set after the file is unpacked.) + FileAttributes attributesToClear = + FileAttributes.ReadOnly | FileAttributes.Hidden | FileAttributes.System; + if ((file.Attributes & attributesToClear) != 0) + { + file.Attributes &= ~attributesToClear; + } + } + + if (!file.Directory.Exists) + { + file.Directory.Create(); + } + + return File.Open( + filePath, FileMode.Create, FileAccess.Write, FileShare.None); + } + + /// + /// Closes a stream where an extracted file was written. + /// + /// The path of the file within the archive. + /// The stream that was previously returned by + /// and is now ready to be closed. + /// The attributes of the extracted file. + /// The last write time of the file. + /// + /// After closing the extracted file stream, this method applies the date + /// and attributes to that file. + /// + public virtual void CloseFileWriteStream( + string path, + Stream stream, + FileAttributes attributes, + DateTime lastWriteTime) + { + if (stream != null) + { +#if CORECLR + stream.Dispose(); +#else + stream.Close(); +#endif + } + + string filePath = this.TranslateFilePath(path); + if (filePath != null) + { + FileInfo file = new FileInfo(filePath); + + if (lastWriteTime != DateTime.MinValue) + { + try + { + file.LastWriteTime = lastWriteTime; + } + catch (ArgumentException) + { + } + catch (IOException) + { + } + } + + try + { + file.Attributes = attributes; + } + catch (IOException) + { + } + } + } + + #endregion + + #region Private utility methods + + /// + /// Translates an internal file path to an external file path using the + /// and the mapping, according to + /// rules documented in and + /// . + /// + /// The path of the file with the archive. + /// The external path of the file, or null if there is no + /// valid translation. + private string TranslateFilePath(string path) + { + string filePath; + if (this.files != null) + { + filePath = this.files[path]; + } + else + { + filePath = path; + } + + if (filePath != null) + { + if (this.directory != null) + { + filePath = Path.Combine(this.directory, filePath); + } + } + + return filePath; + } + + #endregion + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveInfo.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveInfo.cs new file mode 100644 index 000000000..d1d4b4753 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveInfo.cs @@ -0,0 +1,774 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Text.RegularExpressions; + + /// + /// Abstract object representing a compressed archive on disk; + /// provides access to file-based operations on the archive. + /// + public abstract class ArchiveInfo : FileSystemInfo + { + /// + /// Creates a new ArchiveInfo object representing an archive in a + /// specified path. + /// + /// The path to the archive. When creating an archive, + /// this file does not necessarily exist yet. + protected ArchiveInfo(string path) + : base() + { + if (path == null) + { + throw new ArgumentNullException("path"); + } + + // protected instance members inherited from FileSystemInfo: + this.OriginalPath = path; + this.FullPath = Path.GetFullPath(path); + } + + /// + /// Gets the directory that contains the archive. + /// + /// A DirectoryInfo object representing the parent directory of the + /// archive. + public DirectoryInfo Directory + { + get + { + return new DirectoryInfo(Path.GetDirectoryName(this.FullName)); + } + } + + /// + /// Gets the full path of the directory that contains the archive. + /// + /// The full path of the directory that contains the archive. + public string DirectoryName + { + get + { + return Path.GetDirectoryName(this.FullName); + } + } + + /// + /// Gets the size of the archive. + /// + /// The size of the archive in bytes. + public long Length + { + get + { + return new FileInfo(this.FullName).Length; + } + } + + /// + /// Gets the file name of the archive. + /// + /// The file name of the archive, not including any path. + public override string Name + { + get + { + return Path.GetFileName(this.FullName); + } + } + + /// + /// Checks if the archive exists. + /// + /// True if the archive exists; else false. + public override bool Exists + { + get + { + return File.Exists(this.FullName); + } + } + + /// + /// Gets the full path of the archive. + /// + /// The full path of the archive. + public override string ToString() + { + return this.FullName; + } + + /// + /// Deletes the archive. + /// + public override void Delete() + { + File.Delete(this.FullName); + } + + /// + /// Copies an existing archive to another location. + /// + /// The destination file path. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void CopyTo(string destFileName) + { + File.Copy(this.FullName, destFileName); + } + + /// + /// Copies an existing archive to another location, optionally + /// overwriting the destination file. + /// + /// The destination file path. + /// If true, the destination file will be + /// overwritten if it exists. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void CopyTo(string destFileName, bool overwrite) + { + File.Copy(this.FullName, destFileName, overwrite); + } + + /// + /// Moves an existing archive to another location. + /// + /// The destination file path. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void MoveTo(string destFileName) + { + File.Move(this.FullName, destFileName); + this.FullPath = Path.GetFullPath(destFileName); + } + + /// + /// Checks if the archive contains a valid archive header. + /// + /// True if the file is a valid archive; false otherwise. + public bool IsValid() + { + using (Stream stream = File.OpenRead(this.FullName)) + { + using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) + { + return compressionEngine.FindArchiveOffset(stream) >= 0; + } + } + } + + /// + /// Gets information about the files contained in the archive. + /// + /// A list of objects, each + /// containing information about a file in the archive. + public IList GetFiles() + { + return this.InternalGetFiles((Predicate)null); + } + + /// + /// Gets information about the certain files contained in the archive file. + /// + /// The search string, such as + /// "*.txt". + /// A list of objects, each containing + /// information about a file in the archive. + public IList GetFiles(string searchPattern) + { + if (searchPattern == null) + { + throw new ArgumentNullException("searchPattern"); + } + + string regexPattern = String.Format( + CultureInfo.InvariantCulture, + "^{0}$", + Regex.Escape(searchPattern).Replace("\\*", ".*").Replace("\\?", ".")); + Regex regex = new Regex( + regexPattern, + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + return this.InternalGetFiles( + delegate(string match) + { + return regex.IsMatch(match); + }); + } + + /// + /// Extracts all files from an archive to a destination directory. + /// + /// Directory where the files are to be + /// extracted. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void Unpack(string destDirectory) + { + this.Unpack(destDirectory, null); + } + + /// + /// Extracts all files from an archive to a destination directory, + /// optionally extracting only newer files. + /// + /// Directory where the files are to be + /// extracted. + /// Handler for receiving progress + /// information; this may be null if progress is not desired. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void Unpack( + string destDirectory, + EventHandler progressHandler) + { + using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) + { + compressionEngine.Progress += progressHandler; + ArchiveFileStreamContext streamContext = + new ArchiveFileStreamContext(this.FullName, destDirectory, null); + streamContext.EnableOffsetOpen = true; + compressionEngine.Unpack(streamContext, null); + } + } + + /// + /// Extracts a single file from the archive. + /// + /// The name of the file in the archive. Also + /// includes the internal path of the file, if any. File name matching + /// is case-insensitive. + /// The path where the file is to be + /// extracted on disk. + /// If already exists, + /// it will be overwritten. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void UnpackFile(string fileName, string destFileName) + { + if (fileName == null) + { + throw new ArgumentNullException("fileName"); + } + + if (destFileName == null) + { + throw new ArgumentNullException("destFileName"); + } + + this.UnpackFiles( + new string[] { fileName }, + null, + new string[] { destFileName }); + } + + /// + /// Extracts multiple files from the archive. + /// + /// The names of the files in the archive. + /// Each name includes the internal path of the file, if any. File name + /// matching is case-insensitive. + /// This parameter may be null, but if + /// specified it is the root directory for any relative paths in + /// . + /// The paths where the files are to be + /// extracted on disk. If this parameter is null, the files will be + /// extracted with the names from the archive. + /// + /// If any extracted files already exist on disk, they will be overwritten. + ///

The and + /// parameters cannot both be null.

+ ///
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void UnpackFiles( + IList fileNames, + string destDirectory, + IList destFileNames) + { + this.UnpackFiles(fileNames, destDirectory, destFileNames, null); + } + + /// + /// Extracts multiple files from the archive, optionally extracting + /// only newer files. + /// + /// The names of the files in the archive. + /// Each name includes the internal path of the file, if any. File name + /// matching is case-insensitive. + /// This parameter may be null, but if + /// specified it is the root directory for any relative paths in + /// . + /// The paths where the files are to be + /// extracted on disk. If this parameter is null, the files will be + /// extracted with the names from the archive. + /// Handler for receiving progress information; + /// this may be null if progress is not desired. + /// + /// If any extracted files already exist on disk, they will be overwritten. + ///

The and + /// parameters cannot both be null.

+ ///
+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void UnpackFiles( + IList fileNames, + string destDirectory, + IList destFileNames, + EventHandler progressHandler) + { + if (fileNames == null) + { + throw new ArgumentNullException("fileNames"); + } + + if (destFileNames == null) + { + if (destDirectory == null) + { + throw new ArgumentNullException("destFileNames"); + } + + destFileNames = fileNames; + } + + if (destFileNames.Count != fileNames.Count) + { + throw new ArgumentOutOfRangeException("destFileNames"); + } + + IDictionary files = + ArchiveInfo.CreateStringDictionary(fileNames, destFileNames); + this.UnpackFileSet(files, destDirectory, progressHandler); + } + + /// + /// Extracts multiple files from the archive. + /// + /// A mapping from internal file paths to + /// external file paths. Case-senstivity when matching internal paths + /// depends on the IDictionary implementation. + /// This parameter may be null, but if + /// specified it is the root directory for any relative external paths + /// in . + /// + /// If any extracted files already exist on disk, they will be overwritten. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void UnpackFileSet( + IDictionary fileNames, + string destDirectory) + { + this.UnpackFileSet(fileNames, destDirectory, null); + } + + /// + /// Extracts multiple files from the archive. + /// + /// A mapping from internal file paths to + /// external file paths. Case-senstivity when matching internal + /// paths depends on the IDictionary implementation. + /// This parameter may be null, but if + /// specified it is the root directory for any relative external + /// paths in . + /// Handler for receiving progress + /// information; this may be null if progress is not desired. + /// + /// If any extracted files already exist on disk, they will be overwritten. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "dest")] + public void UnpackFileSet( + IDictionary fileNames, + string destDirectory, + EventHandler progressHandler) + { + if (fileNames == null) + { + throw new ArgumentNullException("fileNames"); + } + + using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) + { + compressionEngine.Progress += progressHandler; + ArchiveFileStreamContext streamContext = + new ArchiveFileStreamContext(this.FullName, destDirectory, fileNames); + streamContext.EnableOffsetOpen = true; + compressionEngine.Unpack( + streamContext, + delegate(string match) + { + return fileNames.ContainsKey(match); + }); + } + } + + /// + /// Opens a file inside the archive for reading without actually + /// extracting the file to disk. + /// + /// The name of the file in the archive. Also + /// includes the internal path of the file, if any. File name matching + /// is case-insensitive. + /// + /// A stream for reading directly from the packed file. Like any stream + /// this should be closed/disposed as soon as it is no longer needed. + /// + public Stream OpenRead(string fileName) + { + Stream archiveStream = File.OpenRead(this.FullName); + CompressionEngine compressionEngine = this.CreateCompressionEngine(); + Stream fileStream = compressionEngine.Unpack(archiveStream, fileName); + + // Attach the archiveStream and compressionEngine to the + // fileStream so they get disposed when the fileStream is disposed. + return new CargoStream(fileStream, archiveStream, compressionEngine); + } + + /// + /// Opens a file inside the archive for reading text with UTF-8 encoding + /// without actually extracting the file to disk. + /// + /// The name of the file in the archive. Also + /// includes the internal path of the file, if any. File name matching + /// is case-insensitive. + /// + /// A reader for reading text directly from the packed file. Like any reader + /// this should be closed/disposed as soon as it is no longer needed. + /// + /// + /// To open an archived text file with different encoding, use the + /// method and pass the returned stream to one of + /// the constructor overloads. + /// + public StreamReader OpenText(string fileName) + { + return new StreamReader(this.OpenRead(fileName)); + } + + /// + /// Compresses all files in a directory into the archive. + /// Does not include subdirectories. + /// + /// The directory containing the + /// files to be included. + /// + /// Uses maximum compression level. + /// + public void Pack(string sourceDirectory) + { + this.Pack(sourceDirectory, false, CompressionLevel.Max, null); + } + + /// + /// Compresses all files in a directory into the archive, optionally + /// including subdirectories. + /// + /// This is the root directory + /// for to pack all files. + /// If true, recursively include + /// files in subdirectories. + /// The compression level used when creating + /// the archive. + /// Handler for receiving progress information; + /// this may be null if progress is not desired. + /// + /// The files are stored in the archive using their relative file paths in + /// the directory tree, if supported by the archive file format. + /// + public void Pack( + string sourceDirectory, + bool includeSubdirectories, + CompressionLevel compLevel, + EventHandler progressHandler) + { + IList files = this.GetRelativeFilePathsInDirectoryTree( + sourceDirectory, includeSubdirectories); + this.PackFiles(sourceDirectory, files, files, compLevel, progressHandler); + } + + /// + /// Compresses files into the archive, specifying the names used to + /// store the files in the archive. + /// + /// This parameter may be null, but + /// if specified it is the root directory + /// for any relative paths in . + /// The list of files to be included in + /// the archive. + /// The names of the files as they are stored + /// in the archive. Each name + /// includes the internal path of the file, if any. This parameter may + /// be null, in which case the files are stored in the archive with their + /// source file names and no path information. + /// + /// Uses maximum compression level. + ///

Duplicate items in the array will cause + /// an .

+ ///
+ public void PackFiles( + string sourceDirectory, + IList sourceFileNames, + IList fileNames) + { + this.PackFiles( + sourceDirectory, + sourceFileNames, + fileNames, + CompressionLevel.Max, + null); + } + + /// + /// Compresses files into the archive, specifying the names used to + /// store the files in the archive. + /// + /// This parameter may be null, but if + /// specified it is the root directory + /// for any relative paths in . + /// The list of files to be included in + /// the archive. + /// The names of the files as they are stored in + /// the archive. Each name includes the internal path of the file, if any. + /// This parameter may be null, in which case the files are stored in the + /// archive with their source file names and no path information. + /// The compression level used when creating the + /// archive. + /// Handler for receiving progress information; + /// this may be null if progress is not desired. + /// + /// Duplicate items in the array will cause + /// an . + /// + public void PackFiles( + string sourceDirectory, + IList sourceFileNames, + IList fileNames, + CompressionLevel compLevel, + EventHandler progressHandler) + { + if (sourceFileNames == null) + { + throw new ArgumentNullException("sourceFileNames"); + } + + if (fileNames == null) + { + string[] fileNamesArray = new string[sourceFileNames.Count]; + for (int i = 0; i < sourceFileNames.Count; i++) + { + fileNamesArray[i] = Path.GetFileName(sourceFileNames[i]); + } + + fileNames = fileNamesArray; + } + else if (fileNames.Count != sourceFileNames.Count) + { + throw new ArgumentOutOfRangeException("fileNames"); + } + + using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) + { + compressionEngine.Progress += progressHandler; + IDictionary contextFiles = + ArchiveInfo.CreateStringDictionary(fileNames, sourceFileNames); + ArchiveFileStreamContext streamContext = new ArchiveFileStreamContext( + this.FullName, sourceDirectory, contextFiles); + streamContext.EnableOffsetOpen = true; + compressionEngine.CompressionLevel = compLevel; + compressionEngine.Pack(streamContext, fileNames); + } + } + + /// + /// Compresses files into the archive, specifying the names used + /// to store the files in the archive. + /// + /// This parameter may be null, but if + /// specified it is the root directory + /// for any relative paths in . + /// A mapping from internal file paths to + /// external file paths. + /// + /// Uses maximum compression level. + /// + public void PackFileSet( + string sourceDirectory, + IDictionary fileNames) + { + this.PackFileSet(sourceDirectory, fileNames, CompressionLevel.Max, null); + } + + /// + /// Compresses files into the archive, specifying the names used to + /// store the files in the archive. + /// + /// This parameter may be null, but if + /// specified it is the root directory + /// for any relative paths in . + /// A mapping from internal file paths to + /// external file paths. + /// The compression level used when creating + /// the archive. + /// Handler for receiving progress information; + /// this may be null if progress is not desired. + public void PackFileSet( + string sourceDirectory, + IDictionary fileNames, + CompressionLevel compLevel, + EventHandler progressHandler) + { + if (fileNames == null) + { + throw new ArgumentNullException("fileNames"); + } + + string[] fileNamesArray = new string[fileNames.Count]; + fileNames.Keys.CopyTo(fileNamesArray, 0); + + using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) + { + compressionEngine.Progress += progressHandler; + ArchiveFileStreamContext streamContext = new ArchiveFileStreamContext( + this.FullName, sourceDirectory, fileNames); + streamContext.EnableOffsetOpen = true; + compressionEngine.CompressionLevel = compLevel; + compressionEngine.Pack(streamContext, fileNamesArray); + } + } + + /// + /// Given a directory, gets the relative paths of all files in the + /// directory, optionally including all subdirectories. + /// + /// The directory to search. + /// True to include subdirectories + /// in the search. + /// A list of file paths relative to the directory. + internal IList GetRelativeFilePathsInDirectoryTree( + string dir, bool includeSubdirectories) + { + IList fileList = new List(); + this.RecursiveGetRelativeFilePathsInDirectoryTree( + dir, String.Empty, includeSubdirectories, fileList); + return fileList; + } + + /// + /// Retrieves information about one file from this archive. + /// + /// Path of the file in the archive. + /// File information, or null if the file was not found + /// in the archive. + internal ArchiveFileInfo GetFile(string path) + { + IList files = this.InternalGetFiles( + delegate(string match) + { + return String.Compare( + match, path, StringComparison.OrdinalIgnoreCase) == 0; + }); + return (files != null && files.Count > 0 ? files[0] : null); + } + + /// + /// Creates a compression engine that does the low-level work for + /// this object. + /// + /// A new compression engine instance that matches the specific + /// subclass of archive. + /// + /// Each instance will be d + /// immediately after use. + /// + protected abstract CompressionEngine CreateCompressionEngine(); + + /// + /// Creates a case-insensitive dictionary mapping from one list of + /// strings to the other. + /// + /// List of keys. + /// List of values that are mapped 1-to-1 to + /// the keys. + /// A filled dictionary of the strings. + private static IDictionary CreateStringDictionary( + IList keys, IList values) + { + IDictionary stringDict = + new Dictionary(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < keys.Count; i++) + { + stringDict.Add(keys[i], values[i]); + } + + return stringDict; + } + + /// + /// Recursive-descent helper function for + /// GetRelativeFilePathsInDirectoryTree. + /// + /// The root directory of the search. + /// The relative directory to be + /// processed now. + /// True to descend into + /// subdirectories. + /// List of files found so far. + private void RecursiveGetRelativeFilePathsInDirectoryTree( + string dir, + string relativeDir, + bool includeSubdirectories, + IList fileList) + { + foreach (string file in System.IO.Directory.GetFiles(dir)) + { + string fileName = Path.GetFileName(file); + fileList.Add(Path.Combine(relativeDir, fileName)); + } + + if (includeSubdirectories) + { + foreach (string subDir in System.IO.Directory.GetDirectories(dir)) + { + string subDirName = Path.GetFileName(subDir); + this.RecursiveGetRelativeFilePathsInDirectoryTree( + Path.Combine(dir, subDirName), + Path.Combine(relativeDir, subDirName), + includeSubdirectories, + fileList); + } + } + } + + /// + /// Uses a CompressionEngine to get ArchiveFileInfo objects from this + /// archive, and then associates them with this ArchiveInfo instance. + /// + /// Optional predicate that can determine + /// which files to process. + /// A list of objects, each + /// containing information about a file in the archive. + private IList InternalGetFiles(Predicate fileFilter) + { + using (CompressionEngine compressionEngine = this.CreateCompressionEngine()) + { + ArchiveFileStreamContext streamContext = + new ArchiveFileStreamContext(this.FullName, null, null); + streamContext.EnableOffsetOpen = true; + IList files = + compressionEngine.GetFileInfo(streamContext, fileFilter); + for (int i = 0; i < files.Count; i++) + { + files[i].Archive = this; + } + + return files; + } + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveProgressEventArgs.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveProgressEventArgs.cs new file mode 100644 index 000000000..946f233e4 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveProgressEventArgs.cs @@ -0,0 +1,312 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression +{ + using System; + + /// + /// Contains the data reported in an archive progress event. + /// + public class ArchiveProgressEventArgs : EventArgs + { + private ArchiveProgressType progressType; + + private string currentFileName; + private int currentFileNumber; + private int totalFiles; + private long currentFileBytesProcessed; + private long currentFileTotalBytes; + + private string currentArchiveName; + private short currentArchiveNumber; + private short totalArchives; + private long currentArchiveBytesProcessed; + private long currentArchiveTotalBytes; + + private long fileBytesProcessed; + private long totalFileBytes; + + /// + /// Creates a new ArchiveProgressEventArgs object from specified event parameters. + /// + /// type of status message + /// name of the file being processed + /// number of the current file being processed + /// total number of files to be processed + /// number of bytes processed so far when compressing or extracting a file + /// total number of bytes in the current file + /// name of the current Archive + /// current Archive number, when processing a chained set of Archives + /// total number of Archives in a chained set + /// number of compressed bytes processed so far during an extraction + /// total number of compressed bytes to be processed during an extraction + /// number of uncompressed file bytes processed so far + /// total number of uncompressed file bytes to be processed + public ArchiveProgressEventArgs( + ArchiveProgressType progressType, + string currentFileName, + int currentFileNumber, + int totalFiles, + long currentFileBytesProcessed, + long currentFileTotalBytes, + string currentArchiveName, + int currentArchiveNumber, + int totalArchives, + long currentArchiveBytesProcessed, + long currentArchiveTotalBytes, + long fileBytesProcessed, + long totalFileBytes) + { + this.progressType = progressType; + this.currentFileName = currentFileName; + this.currentFileNumber = currentFileNumber; + this.totalFiles = totalFiles; + this.currentFileBytesProcessed = currentFileBytesProcessed; + this.currentFileTotalBytes = currentFileTotalBytes; + this.currentArchiveName = currentArchiveName; + this.currentArchiveNumber = (short) currentArchiveNumber; + this.totalArchives = (short) totalArchives; + this.currentArchiveBytesProcessed = currentArchiveBytesProcessed; + this.currentArchiveTotalBytes = currentArchiveTotalBytes; + this.fileBytesProcessed = fileBytesProcessed; + this.totalFileBytes = totalFileBytes; + } + + /// + /// Gets the type of status message. + /// + /// A value indicating what type of progress event occurred. + /// + /// The handler may choose to ignore some types of progress events. + /// For example, if the handler will only list each file as it is + /// compressed/extracted, it can ignore events that + /// are not of type . + /// + public ArchiveProgressType ProgressType + { + get + { + return this.progressType; + } + } + + /// + /// Gets the name of the file being processed. (The name of the file within the Archive; not the external + /// file path.) Also includes the internal path of the file, if any. Valid for + /// , , + /// and messages. + /// + /// The name of the file currently being processed, or null if processing + /// is currently at the stream or archive level. + public string CurrentFileName + { + get + { + return this.currentFileName; + } + } + + /// + /// Gets the number of the current file being processed. The first file is number 0, and the last file + /// is -1. Valid for , + /// , and messages. + /// + /// The number of the file currently being processed, or the most recent + /// file processed if processing is currently at the stream or archive level. + public int CurrentFileNumber + { + get + { + return this.currentFileNumber; + } + } + + /// + /// Gets the total number of files to be processed. Valid for all message types. + /// + /// The total number of files to be processed that are known so far. + public int TotalFiles + { + get + { + return this.totalFiles; + } + } + + /// + /// Gets the number of bytes processed so far when compressing or extracting a file. Valid for + /// , , + /// and messages. + /// + /// The number of uncompressed bytes processed so far for the current file, + /// or 0 if processing is currently at the stream or archive level. + public long CurrentFileBytesProcessed + { + get + { + return this.currentFileBytesProcessed; + } + } + + /// + /// Gets the total number of bytes in the current file. Valid for , + /// , and messages. + /// + /// The uncompressed size of the current file being processed, + /// or 0 if processing is currently at the stream or archive level. + public long CurrentFileTotalBytes + { + get + { + return this.currentFileTotalBytes; + } + } + + /// + /// Gets the name of the current archive. Not necessarily the name of the archive on disk. + /// Valid for all message types. + /// + /// The name of the current archive, or an empty string if no name was specified. + public string CurrentArchiveName + { + get + { + return this.currentArchiveName; + } + } + + /// + /// Gets the current archive number, when processing a chained set of archives. Valid for all message types. + /// + /// The number of the current archive. + /// The first archive is number 0, and the last archive is + /// -1. + public int CurrentArchiveNumber + { + get + { + return this.currentArchiveNumber; + } + } + + /// + /// Gets the total number of known archives in a chained set. Valid for all message types. + /// + /// The total number of known archives in a chained set. + /// + /// When using the compression option to auto-split into multiple archives based on data size, + /// this value will not be accurate until the end. + /// + public int TotalArchives + { + get + { + return this.totalArchives; + } + } + + /// + /// Gets the number of compressed bytes processed so far during extraction + /// of the current archive. Valid for all extraction messages. + /// + /// The number of compressed bytes processed so far during extraction + /// of the current archive. + public long CurrentArchiveBytesProcessed + { + get + { + return this.currentArchiveBytesProcessed; + } + } + + /// + /// Gets the total number of compressed bytes to be processed during extraction + /// of the current archive. Valid for all extraction messages. + /// + /// The total number of compressed bytes to be processed during extraction + /// of the current archive. + public long CurrentArchiveTotalBytes + { + get + { + return this.currentArchiveTotalBytes; + } + } + + /// + /// Gets the number of uncompressed bytes processed so far among all files. Valid for all message types. + /// + /// The number of uncompressed file bytes processed so far among all files. + /// + /// When compared to , this can be used as a measure of overall progress. + /// + public long FileBytesProcessed + { + get + { + return this.fileBytesProcessed; + } + } + + /// + /// Gets the total number of uncompressed file bytes to be processed. Valid for all message types. + /// + /// The total number of uncompressed bytes to be processed among all files. + public long TotalFileBytes + { + get + { + return this.totalFileBytes; + } + } + +#if DEBUG + + /// + /// Creates a string representation of the progress event. + /// + /// a listing of all event parameters and values + public override string ToString() + { + string formatString = + "{0}\n" + + "\t CurrentFileName = {1}\n" + + "\t CurrentFileNumber = {2}\n" + + "\t TotalFiles = {3}\n" + + "\t CurrentFileBytesProcessed = {4}\n" + + "\t CurrentFileTotalBytes = {5}\n" + + "\t CurrentArchiveName = {6}\n" + + "\t CurrentArchiveNumber = {7}\n" + + "\t TotalArchives = {8}\n" + + "\t CurrentArchiveBytesProcessed = {9}\n" + + "\t CurrentArchiveTotalBytes = {10}\n" + + "\t FileBytesProcessed = {11}\n" + + "\t TotalFileBytes = {12}\n"; + return String.Format( + System.Globalization.CultureInfo.InvariantCulture, + formatString, + this.ProgressType, + this.CurrentFileName, + this.CurrentFileNumber, + this.TotalFiles, + this.CurrentFileBytesProcessed, + this.CurrentFileTotalBytes, + this.CurrentArchiveName, + this.CurrentArchiveNumber, + this.TotalArchives, + this.CurrentArchiveBytesProcessed, + this.CurrentArchiveTotalBytes, + this.FileBytesProcessed, + this.TotalFileBytes); + } + +#endif + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveProgressType.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveProgressType.cs new file mode 100644 index 000000000..8b1ff5b37 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/ArchiveProgressType.cs @@ -0,0 +1,72 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression +{ + /// + /// The type of progress event. + /// + /// + ///

PACKING EXAMPLE: The following sequence of events might be received when + /// extracting a simple archive file with 2 files.

+ /// + /// Message TypeDescription + /// StartArchive Begin extracting archive + /// StartFile Begin extracting first file + /// PartialFile Extracting first file + /// PartialFile Extracting first file + /// FinishFile Finished extracting first file + /// StartFile Begin extracting second file + /// PartialFile Extracting second file + /// FinishFile Finished extracting second file + /// FinishArchiveFinished extracting archive + /// + ///

+ ///

UNPACKING EXAMPLE: Packing 3 files into 2 archive chunks, where the second file is + /// continued to the second archive chunk.

+ /// + /// Message TypeDescription + /// StartFile Begin compressing first file + /// FinishFile Finished compressing first file + /// StartFile Begin compressing second file + /// PartialFile Compressing second file + /// PartialFile Compressing second file + /// FinishFile Finished compressing second file + /// StartArchive Begin writing first archive + /// PartialArchiveWriting first archive + /// FinishArchive Finished writing first archive + /// StartFile Begin compressing third file + /// PartialFile Compressing third file + /// FinishFile Finished compressing third file + /// StartArchive Begin writing second archive + /// PartialArchiveWriting second archive + /// FinishArchive Finished writing second archive + /// + ///
+ public enum ArchiveProgressType : int + { + /// Status message before beginning the packing or unpacking an individual file. + StartFile, + + /// Status message (possibly reported multiple times) during the process of packing or unpacking a file. + PartialFile, + + /// Status message after completion of the packing or unpacking an individual file. + FinishFile, + + /// Status message before beginning the packing or unpacking an archive. + StartArchive, + + /// Status message (possibly reported multiple times) during the process of packing or unpacking an archiv. + PartialArchive, + + /// Status message after completion of the packing or unpacking of an archive. + FinishArchive, + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/BasicUnpackStreamContext.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/BasicUnpackStreamContext.cs new file mode 100644 index 000000000..a647a3f50 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/BasicUnpackStreamContext.cs @@ -0,0 +1,97 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.IO; + + /// + /// Stream context used to extract a single file from an archive into a memory stream. + /// + [SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable")] + public class BasicUnpackStreamContext : IUnpackStreamContext + { + private Stream archiveStream; + private Stream fileStream; + + /// + /// Creates a new BasicExtractStreamContext that reads from the specified archive stream. + /// + /// Archive stream to read from. + public BasicUnpackStreamContext(Stream archiveStream) + { + this.archiveStream = archiveStream; + } + + /// + /// Gets the stream for the extracted file, or null if no file was extracted. + /// + public Stream FileStream + { + get + { + return this.fileStream; + } + } + + /// + /// Opens the archive stream for reading. Returns a DuplicateStream instance, + /// so the stream may be virtually opened multiple times. + /// + /// The archive number to open (ignored; 0 is assumed). + /// The name of the archive being opened. + /// Instance of the compression engine doing the operations. + /// A stream from which archive bytes are read. + public Stream OpenArchiveReadStream(int archiveNumber, string archiveName, CompressionEngine compressionEngine) + { + return new DuplicateStream(this.archiveStream); + } + + /// + /// Does *not* close the stream. The archive stream should be managed by + /// the code that invokes the archive extraction. + /// + /// The archive number of the stream to close. + /// The name of the archive being closed. + /// The stream being closed. + public void CloseArchiveReadStream(int archiveNumber, string archiveName, Stream stream) + { + // Do nothing. + } + + /// + /// Opens a stream for writing extracted file bytes. The returned stream is a MemoryStream + /// instance, so the file is extracted straight into memory. + /// + /// Path of the file within the archive. + /// The uncompressed size of the file to be extracted. + /// The last write time of the file. + /// A stream where extracted file bytes are to be written. + public Stream OpenFileWriteStream(string path, long fileSize, DateTime lastWriteTime) + { + this.fileStream = new MemoryStream(new byte[fileSize], 0, (int) fileSize, true, true); + return this.fileStream; + } + + /// + /// Does *not* close the file stream. The file stream is saved in memory so it can + /// be read later. + /// + /// Path of the file within the archive. + /// The file stream to be closed. + /// The attributes of the extracted file. + /// The last write time of the file. + public void CloseFileWriteStream(string path, Stream stream, FileAttributes attributes, DateTime lastWriteTime) + { + // Do nothing. + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabEngine.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabEngine.cs new file mode 100644 index 000000000..68f7d76f7 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabEngine.cs @@ -0,0 +1,171 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Cab +{ + using System; + using System.Collections.Generic; + using System.IO; + + /// + /// Engine capable of packing and unpacking archives in the cabinet format. + /// + public class CabEngine : CompressionEngine + { + private CabPacker packer; + private CabUnpacker unpacker; + + /// + /// Creates a new instance of the cabinet engine. + /// + public CabEngine() + : base() + { + } + + /// + /// Disposes of resources allocated by the cabinet engine. + /// + /// If true, the method has been called directly + /// or indirectly by a user's code, so managed and unmanaged resources + /// will be disposed. If false, the method has been called by the runtime + /// from inside the finalizer, and only unmanaged resources will be + /// disposed. + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (packer != null) + { + packer.Dispose(); + packer = null; + } + if (unpacker != null) + { + unpacker.Dispose(); + unpacker = null; + } + } + + base.Dispose(disposing); + } + + private CabPacker Packer + { + get + { + if (this.packer == null) + { + this.packer = new CabPacker(this); + } + + return this.packer; + } + } + + private CabUnpacker Unpacker + { + get + { + if (this.unpacker == null) + { + this.unpacker = new CabUnpacker(this); + } + + return this.unpacker; + } + } + + /// + /// Creates a cabinet or chain of cabinets. + /// + /// A context interface to handle opening + /// and closing of cabinet and file streams. + /// The paths of the files in the archive (not + /// external file paths). + /// The maximum number of bytes for one + /// cabinet before the contents are chained to the next cabinet, or zero + /// for unlimited cabinet size. + /// The cabinet could not be + /// created. + /// + /// The stream context implementation may provide a mapping from the + /// file paths within the cabinet to the external file paths. + /// Smaller folder sizes can make it more efficient to extract + /// individual files out of large cabinet packages. + /// + public override void Pack( + IPackStreamContext streamContext, + IEnumerable files, + long maxArchiveSize) + { + this.Packer.CompressionLevel = this.CompressionLevel; + this.Packer.UseTempFiles = this.UseTempFiles; + this.Packer.Pack(streamContext, files, maxArchiveSize); + } + + /// + /// Checks whether a Stream begins with a header that indicates + /// it is a valid cabinet file. + /// + /// Stream for reading the cabinet file. + /// True if the stream is a valid cabinet file + /// (with no offset); false otherwise. + public override bool IsArchive(Stream stream) + { + return this.Unpacker.IsArchive(stream); + } + + /// + /// Gets information about files in a cabinet or cabinet chain. + /// + /// A context interface to handle opening + /// and closing of cabinet and file streams. + /// A predicate that can determine + /// which files to process, optional. + /// Information about files in the cabinet stream. + /// The cabinet provided + /// by the stream context is not valid. + /// + /// The predicate takes an internal file + /// path and returns true to include the file or false to exclude it. + /// + public override IList GetFileInfo( + IUnpackStreamContext streamContext, + Predicate fileFilter) + { + return this.Unpacker.GetFileInfo(streamContext, fileFilter); + } + + /// + /// Extracts files from a cabinet or cabinet chain. + /// + /// A context interface to handle opening + /// and closing of cabinet and file streams. + /// An optional predicate that can determine + /// which files to process. + /// The cabinet provided + /// by the stream context is not valid. + /// + /// The predicate takes an internal file + /// path and returns true to include the file or false to exclude it. + /// + public override void Unpack( + IUnpackStreamContext streamContext, + Predicate fileFilter) + { + this.Unpacker.Unpack(streamContext, fileFilter); + } + + internal void ReportProgress(ArchiveProgressEventArgs e) + { + base.OnProgress(e); + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabException.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabException.cs new file mode 100644 index 000000000..3aa9df862 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabException.cs @@ -0,0 +1,132 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Cab +{ + using System; + using System.Reflection; + using System.Globalization; + using System.Resources; + + /// + /// Exception class for cabinet operations. + /// + public class CabException : ArchiveException + { + private static ResourceManager errorResources; + private int error; + private int errorCode; + + /// + /// Creates a new CabException with a specified error message and a reference to the + /// inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. If the + /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception + /// is raised in a catch block that handles the inner exception. + public CabException(string message, Exception innerException) + : this(0, 0, message, innerException) { } + + /// + /// Creates a new CabException with a specified error message. + /// + /// The message that describes the error. + public CabException(string message) + : this(0, 0, message, null) { } + + /// + /// Creates a new CabException. + /// + public CabException() + : this(0, 0, null, null) { } + + internal CabException(int error, int errorCode, string message, Exception innerException) + : base(message, innerException) + { + this.error = error; + this.errorCode = errorCode; + } + + internal CabException(int error, int errorCode, string message) + : this(error, errorCode, message, null) { } + + + /// + /// Gets the FCI or FDI cabinet engine error number. + /// + /// A cabinet engine error number, or 0 if the exception was + /// not related to a cabinet engine error number. + public int Error + { + get + { + return this.error; + } + } + + /// + /// Gets the Win32 error code. + /// + /// A Win32 error code, or 0 if the exception was + /// not related to a Win32 error. + public int ErrorCode + { + get + { + return this.errorCode; + } + } + + internal static ResourceManager ErrorResources + { + get + { + if (errorResources == null) + { + errorResources = new ResourceManager( + typeof(CabException).Namespace + ".Errors", +#if CORECLR + typeof(CabException).GetTypeInfo().Assembly); +#else + typeof(CabException).Assembly); +#endif + } + return errorResources; + } + } + + + internal static string GetErrorMessage(int error, int errorCode, bool extracting) + { + const int FCI_ERROR_RESOURCE_OFFSET = 1000; + const int FDI_ERROR_RESOURCE_OFFSET = 2000; + int resourceOffset = (extracting ? FDI_ERROR_RESOURCE_OFFSET : FCI_ERROR_RESOURCE_OFFSET); + + string msg = CabException.ErrorResources.GetString( + (resourceOffset + error).ToString(CultureInfo.InvariantCulture.NumberFormat), + CultureInfo.CurrentUICulture); + + if (msg == null) + { + msg = CabException.ErrorResources.GetString( + resourceOffset.ToString(CultureInfo.InvariantCulture.NumberFormat), + CultureInfo.CurrentUICulture); + } + + if (errorCode != 0) + { + const string GENERIC_ERROR_RESOURCE = "1"; + string msg2 = CabException.ErrorResources.GetString(GENERIC_ERROR_RESOURCE, CultureInfo.CurrentUICulture); + msg = String.Format(CultureInfo.InvariantCulture, "{0} " + msg2, msg, errorCode); + } + return msg; + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabFileInfo.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabFileInfo.cs new file mode 100644 index 000000000..d24c2bfa5 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabFileInfo.cs @@ -0,0 +1,123 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Cab +{ + using System; + using System.IO; + + /// + /// Object representing a compressed file within a cabinet package; provides operations for getting + /// the file properties and extracting the file. + /// + public class CabFileInfo : ArchiveFileInfo + { + private int cabFolder; + + /// + /// Creates a new CabinetFileInfo object representing a file within a cabinet in a specified path. + /// + /// An object representing the cabinet containing the file. + /// The path to the file within the cabinet. Usually, this is a simple file + /// name, but if the cabinet contains a directory structure this may include the directory. + public CabFileInfo(CabInfo cabinetInfo, string filePath) + : base(cabinetInfo, filePath) + { + if (cabinetInfo == null) + { + throw new ArgumentNullException("cabinetInfo"); + } + + this.cabFolder = -1; + } + + /// + /// Creates a new CabinetFileInfo object with all parameters specified, + /// used internally when reading the metadata out of a cab. + /// + /// The internal path and name of the file in the cab. + /// The folder number containing the file. + /// The cabinet number where the file starts. + /// The stored attributes of the file. + /// The stored last write time of the file. + /// The uncompressed size of the file. + internal CabFileInfo( + string filePath, + int cabFolder, + int cabNumber, + FileAttributes attributes, + DateTime lastWriteTime, + long length) + : base(filePath, cabNumber, attributes, lastWriteTime, length) + { + this.cabFolder = cabFolder; + } + + /// + /// Gets or sets the cabinet that contains this file. + /// + /// + /// The CabinetInfo instance that retrieved this file information -- this + /// may be null if the CabinetFileInfo object was returned directly from a + /// stream. + /// + public CabInfo Cabinet + { + get + { + return (CabInfo)this.Archive; + } + } + + /// + /// Gets the full path of the cabinet that contains this file. + /// + /// The full path of the cabinet that contains this file. + public string CabinetName + { + get + { + return this.ArchiveName; + } + } + + /// + /// Gets the number of the folder containing this file. + /// + /// The number of the cabinet folder containing this file. + /// A single folder or the first folder of a cabinet + /// (or chain of cabinets) is numbered 0. + public int CabinetFolderNumber + { + get + { + if (this.cabFolder < 0) + { + this.Refresh(); + } + return this.cabFolder; + } + } + + /// + /// Refreshes the information in this object with new data retrieved + /// from an archive. + /// + /// Fresh instance for the same file just + /// read from the archive. + /// + /// This implementation refreshes the . + /// + protected override void Refresh(ArchiveFileInfo newFileInfo) + { + base.Refresh(newFileInfo); + this.cabFolder = ((CabFileInfo)newFileInfo).cabFolder; + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabInfo.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabInfo.cs new file mode 100644 index 000000000..025c411e4 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabInfo.cs @@ -0,0 +1,77 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Cab +{ + using System; + using System.Collections.Generic; + + /// + /// Object representing a cabinet file on disk; provides access to + /// file-based operations on the cabinet file. + /// + /// + /// Generally, the methods on this class are much easier to use than the + /// stream-based interfaces provided by the class. + /// + public class CabInfo : ArchiveInfo + { + /// + /// Creates a new CabinetInfo object representing a cabinet file in a specified path. + /// + /// The path to the cabinet file. When creating a cabinet file, this file does not + /// necessarily exist yet. + public CabInfo(string path) + : base(path) + { + } + + /// + /// Creates a compression engine that does the low-level work for + /// this object. + /// + /// A new instance. + /// + /// Each instance will be d + /// immediately after use. + /// + protected override CompressionEngine CreateCompressionEngine() + { + return new CabEngine(); + } + + /// + /// Gets information about the files contained in the archive. + /// + /// A list of objects, each + /// containing information about a file in the archive. + public new IList GetFiles() + { + IList files = base.GetFiles(); + List cabFiles = new List(files.Count); + foreach (CabFileInfo cabFile in files) cabFiles.Add(cabFile); + return cabFiles.AsReadOnly(); + } + + /// + /// Gets information about the certain files contained in the archive file. + /// + /// The search string, such as + /// "*.txt". + /// A list of objects, each containing + /// information about a file in the archive. + public new IList GetFiles(string searchPattern) + { + IList files = base.GetFiles(searchPattern); + List cabFiles = new List(files.Count); + foreach (CabFileInfo cabFile in files) cabFiles.Add(cabFile); + return cabFiles.AsReadOnly(); + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabPacker.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabPacker.cs new file mode 100644 index 000000000..2ee02b9a7 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabPacker.cs @@ -0,0 +1,692 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Cab +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Runtime.InteropServices; +#if !CORECLR + using System.Security.Permissions; +#endif + using System.Text; + + internal class CabPacker : CabWorker + { + private const string TempStreamName = "%%TEMP%%"; + + private NativeMethods.FCI.Handle fciHandle; + + // These delegates need to be saved as member variables + // so that they don't get GC'd. + private NativeMethods.FCI.PFNALLOC fciAllocMemHandler; + private NativeMethods.FCI.PFNFREE fciFreeMemHandler; + private NativeMethods.FCI.PFNOPEN fciOpenStreamHandler; + private NativeMethods.FCI.PFNREAD fciReadStreamHandler; + private NativeMethods.FCI.PFNWRITE fciWriteStreamHandler; + private NativeMethods.FCI.PFNCLOSE fciCloseStreamHandler; + private NativeMethods.FCI.PFNSEEK fciSeekStreamHandler; + private NativeMethods.FCI.PFNFILEPLACED fciFilePlacedHandler; + private NativeMethods.FCI.PFNDELETE fciDeleteFileHandler; + private NativeMethods.FCI.PFNGETTEMPFILE fciGetTempFileHandler; + + private NativeMethods.FCI.PFNGETNEXTCABINET fciGetNextCabinet; + private NativeMethods.FCI.PFNSTATUS fciCreateStatus; + private NativeMethods.FCI.PFNGETOPENINFO fciGetOpenInfo; + + private IPackStreamContext context; + + private FileAttributes fileAttributes; + private DateTime fileLastWriteTime; + + private int maxCabBytes; + + private long totalFolderBytesProcessedInCurrentCab; + + private CompressionLevel compressionLevel; + private bool dontUseTempFiles; + private IList tempStreams; + + public CabPacker(CabEngine cabEngine) + : base(cabEngine) + { + this.fciAllocMemHandler = this.CabAllocMem; + this.fciFreeMemHandler = this.CabFreeMem; + this.fciOpenStreamHandler = this.CabOpenStreamEx; + this.fciReadStreamHandler = this.CabReadStreamEx; + this.fciWriteStreamHandler = this.CabWriteStreamEx; + this.fciCloseStreamHandler = this.CabCloseStreamEx; + this.fciSeekStreamHandler = this.CabSeekStreamEx; + this.fciFilePlacedHandler = this.CabFilePlaced; + this.fciDeleteFileHandler = this.CabDeleteFile; + this.fciGetTempFileHandler = this.CabGetTempFile; + this.fciGetNextCabinet = this.CabGetNextCabinet; + this.fciCreateStatus = this.CabCreateStatus; + this.fciGetOpenInfo = this.CabGetOpenInfo; + this.tempStreams = new List(); + this.compressionLevel = CompressionLevel.Normal; + } + + public bool UseTempFiles + { + get + { + return !this.dontUseTempFiles; + } + + set + { + this.dontUseTempFiles = !value; + } + } + + public CompressionLevel CompressionLevel + { + get + { + return this.compressionLevel; + } + + set + { + this.compressionLevel = value; + } + } + + [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")] + private void CreateFci(long maxArchiveSize) + { + NativeMethods.FCI.CCAB ccab = new NativeMethods.FCI.CCAB(); + if (maxArchiveSize > 0 && maxArchiveSize < ccab.cb) + { + ccab.cb = Math.Max( + NativeMethods.FCI.MIN_DISK, (int)maxArchiveSize); + } + + object maxFolderSizeOption = this.context.GetOption( + "maxFolderSize", null); + if (maxFolderSizeOption != null) + { + long maxFolderSize = Convert.ToInt64( + maxFolderSizeOption, CultureInfo.InvariantCulture); + if (maxFolderSize > 0 && maxFolderSize < ccab.cbFolderThresh) + { + ccab.cbFolderThresh = (int)maxFolderSize; + } + } + + this.maxCabBytes = ccab.cb; + ccab.szCab = this.context.GetArchiveName(0); + if (ccab.szCab == null) + { + throw new FileNotFoundException( + "Cabinet name not provided by stream context."); + } + ccab.setID = (short)new Random().Next( + Int16.MinValue, Int16.MaxValue + 1); + this.CabNumbers[ccab.szCab] = 0; + this.currentArchiveName = ccab.szCab; + this.totalArchives = 1; + this.CabStream = null; + + this.Erf.Clear(); + this.fciHandle = NativeMethods.FCI.Create( + this.ErfHandle.AddrOfPinnedObject(), + this.fciFilePlacedHandler, + this.fciAllocMemHandler, + this.fciFreeMemHandler, + this.fciOpenStreamHandler, + this.fciReadStreamHandler, + this.fciWriteStreamHandler, + this.fciCloseStreamHandler, + this.fciSeekStreamHandler, + this.fciDeleteFileHandler, + this.fciGetTempFileHandler, + ccab, + IntPtr.Zero); + this.CheckError(false); + } + + [SuppressMessage("Microsoft.Security", "CA2106:SecureAsserts")] + [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")] +#if !CORECLR + [SecurityPermission(SecurityAction.Assert, UnmanagedCode = true)] +#endif + public void Pack( + IPackStreamContext streamContext, + IEnumerable files, + long maxArchiveSize) + { + if (streamContext == null) + { + throw new ArgumentNullException("streamContext"); + } + + if (files == null) + { + throw new ArgumentNullException("files"); + } + + lock (this) + { + try + { + this.context = streamContext; + + this.ResetProgressData(); + + this.CreateFci(maxArchiveSize); + + foreach (string file in files) + { + FileAttributes attributes; + DateTime lastWriteTime; + Stream fileStream = this.context.OpenFileReadStream( + file, + out attributes, + out lastWriteTime); + if (fileStream != null) + { + this.totalFileBytes += fileStream.Length; + this.totalFiles++; + this.context.CloseFileReadStream(file, fileStream); + } + } + + long uncompressedBytesInFolder = 0; + this.currentFileNumber = -1; + + foreach (string file in files) + { + FileAttributes attributes; + DateTime lastWriteTime; + Stream fileStream = this.context.OpenFileReadStream( + file, out attributes, out lastWriteTime); + if (fileStream == null) + { + continue; + } + + if (fileStream.Length >= (long)NativeMethods.FCI.MAX_FOLDER) + { + throw new NotSupportedException(String.Format( + CultureInfo.InvariantCulture, + "File {0} exceeds maximum file size " + + "for cabinet format.", + file)); + } + + if (uncompressedBytesInFolder > 0) + { + // Automatically create a new folder if this file + // won't fit in the current folder. + bool nextFolder = uncompressedBytesInFolder + + fileStream.Length >= (long)NativeMethods.FCI.MAX_FOLDER; + + // Otherwise ask the client if it wants to + // move to the next folder. + if (!nextFolder) + { + object nextFolderOption = streamContext.GetOption( + "nextFolder", + new object[] { file, this.currentFolderNumber }); + nextFolder = Convert.ToBoolean( + nextFolderOption, CultureInfo.InvariantCulture); + } + + if (nextFolder) + { + this.FlushFolder(); + uncompressedBytesInFolder = 0; + } + } + + if (this.currentFolderTotalBytes > 0) + { + this.currentFolderTotalBytes = 0; + this.currentFolderNumber++; + uncompressedBytesInFolder = 0; + } + + this.currentFileName = file; + this.currentFileNumber++; + + this.currentFileTotalBytes = fileStream.Length; + this.currentFileBytesProcessed = 0; + this.OnProgress(ArchiveProgressType.StartFile); + + uncompressedBytesInFolder += fileStream.Length; + + this.AddFile( + file, + fileStream, + attributes, + lastWriteTime, + false, + this.CompressionLevel); + } + + this.FlushFolder(); + this.FlushCabinet(); + } + finally + { + if (this.CabStream != null) + { + this.context.CloseArchiveWriteStream( + this.currentArchiveNumber, + this.currentArchiveName, + this.CabStream); + this.CabStream = null; + } + + if (this.FileStream != null) + { + this.context.CloseFileReadStream( + this.currentFileName, this.FileStream); + this.FileStream = null; + } + this.context = null; + + if (this.fciHandle != null) + { + this.fciHandle.Dispose(); + this.fciHandle = null; + } + } + } + } + + internal override int CabOpenStreamEx(string path, int openFlags, int shareMode, out int err, IntPtr pv) + { + if (this.CabNumbers.ContainsKey(path)) + { + Stream stream = this.CabStream; + if (stream == null) + { + short cabNumber = this.CabNumbers[path]; + + this.currentFolderTotalBytes = 0; + + stream = this.context.OpenArchiveWriteStream(cabNumber, path, true, this.CabEngine); + if (stream == null) + { + throw new FileNotFoundException( + String.Format(CultureInfo.InvariantCulture, "Cabinet {0} not provided.", cabNumber)); + } + this.currentArchiveName = path; + + this.currentArchiveTotalBytes = Math.Min( + this.totalFolderBytesProcessedInCurrentCab, this.maxCabBytes); + this.currentArchiveBytesProcessed = 0; + + this.OnProgress(ArchiveProgressType.StartArchive); + this.CabStream = stream; + } + path = CabWorker.CabStreamName; + } + else if (path == CabPacker.TempStreamName) + { + // Opening memory stream for a temp file. + Stream stream = new MemoryStream(); + this.tempStreams.Add(stream); + int streamHandle = this.StreamHandles.AllocHandle(stream); + err = 0; + return streamHandle; + } + else if (path != CabWorker.CabStreamName) + { + // Opening a file on disk for a temp file. + path = Path.Combine(Path.GetTempPath(), path); + Stream stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite); + this.tempStreams.Add(stream); + stream = new DuplicateStream(stream); + int streamHandle = this.StreamHandles.AllocHandle(stream); + err = 0; + return streamHandle; + } + return base.CabOpenStreamEx(path, openFlags, shareMode, out err, pv); + } + + internal override int CabWriteStreamEx(int streamHandle, IntPtr memory, int cb, out int err, IntPtr pv) + { + int count = base.CabWriteStreamEx(streamHandle, memory, cb, out err, pv); + if (count > 0 && err == 0) + { + Stream stream = this.StreamHandles[streamHandle]; + if (DuplicateStream.OriginalStream(stream) == + DuplicateStream.OriginalStream(this.CabStream)) + { + this.currentArchiveBytesProcessed += cb; + if (this.currentArchiveBytesProcessed > this.currentArchiveTotalBytes) + { + this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes; + } + } + } + return count; + } + + internal override int CabCloseStreamEx(int streamHandle, out int err, IntPtr pv) + { + Stream stream = DuplicateStream.OriginalStream(this.StreamHandles[streamHandle]); + + if (stream == DuplicateStream.OriginalStream(this.FileStream)) + { + this.context.CloseFileReadStream(this.currentFileName, stream); + this.FileStream = null; + long remainder = this.currentFileTotalBytes - this.currentFileBytesProcessed; + this.currentFileBytesProcessed += remainder; + this.fileBytesProcessed += remainder; + this.OnProgress(ArchiveProgressType.FinishFile); + + this.currentFileTotalBytes = 0; + this.currentFileBytesProcessed = 0; + this.currentFileName = null; + } + else if (stream == DuplicateStream.OriginalStream(this.CabStream)) + { + if (stream.CanWrite) + { + stream.Flush(); + } + + this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes; + this.OnProgress(ArchiveProgressType.FinishArchive); + this.currentArchiveNumber++; + this.totalArchives++; + + this.context.CloseArchiveWriteStream( + this.currentArchiveNumber, + this.currentArchiveName, + stream); + + this.currentArchiveName = this.NextCabinetName; + this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes = 0; + this.totalFolderBytesProcessedInCurrentCab = 0; + + this.CabStream = null; + } + else // Must be a temp stream + { +#if CORECLR + stream.Dispose(); +#else + stream.Close(); +#endif + this.tempStreams.Remove(stream); + } + return base.CabCloseStreamEx(streamHandle, out err, pv); + } + + /// + /// Disposes of resources allocated by the cabinet engine. + /// + /// If true, the method has been called directly or indirectly by a user's code, + /// so managed and unmanaged resources will be disposed. If false, the method has been called by the + /// runtime from inside the finalizer, and only unmanaged resources will be disposed. + [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")] + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (this.fciHandle != null) + { + this.fciHandle.Dispose(); + this.fciHandle = null; + } + } + } + finally + { + base.Dispose(disposing); + } + } + + private static NativeMethods.FCI.TCOMP GetCompressionType(CompressionLevel compLevel) + { + if (compLevel < CompressionLevel.Min) + { + return NativeMethods.FCI.TCOMP.TYPE_NONE; + } + else + { + if (compLevel > CompressionLevel.Max) + { + compLevel = CompressionLevel.Max; + } + + int lzxWindowMax = + ((int)NativeMethods.FCI.TCOMP.LZX_WINDOW_HI >> (int)NativeMethods.FCI.TCOMP.SHIFT_LZX_WINDOW) - + ((int)NativeMethods.FCI.TCOMP.LZX_WINDOW_LO >> (int)NativeMethods.FCI.TCOMP.SHIFT_LZX_WINDOW); + int lzxWindow = lzxWindowMax * + (compLevel - CompressionLevel.Min) / (CompressionLevel.Max - CompressionLevel.Min); + + return (NativeMethods.FCI.TCOMP)((int)NativeMethods.FCI.TCOMP.TYPE_LZX | + ((int)NativeMethods.FCI.TCOMP.LZX_WINDOW_LO + + (lzxWindow << (int)NativeMethods.FCI.TCOMP.SHIFT_LZX_WINDOW))); + } + } + + [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")] + private void AddFile( + string name, + Stream stream, + FileAttributes attributes, + DateTime lastWriteTime, + bool execute, + CompressionLevel compLevel) + { + this.FileStream = stream; + this.fileAttributes = attributes & + (FileAttributes.Archive | FileAttributes.Hidden | FileAttributes.ReadOnly | FileAttributes.System); + this.fileLastWriteTime = lastWriteTime; + this.currentFileName = name; + + NativeMethods.FCI.TCOMP tcomp = CabPacker.GetCompressionType(compLevel); + + IntPtr namePtr = IntPtr.Zero; + try + { + Encoding nameEncoding = Encoding.ASCII; + if (Encoding.UTF8.GetByteCount(name) > name.Length) + { + nameEncoding = Encoding.UTF8; + this.fileAttributes |= FileAttributes.Normal; // _A_NAME_IS_UTF + } + + byte[] nameBytes = nameEncoding.GetBytes(name); + namePtr = Marshal.AllocHGlobal(nameBytes.Length + 1); + Marshal.Copy(nameBytes, 0, namePtr, nameBytes.Length); + Marshal.WriteByte(namePtr, nameBytes.Length, 0); + + this.Erf.Clear(); + var result = NativeMethods.FCI.AddFile( + this.fciHandle, + String.Empty, + namePtr, + execute, + this.fciGetNextCabinet, + this.fciCreateStatus, + this.fciGetOpenInfo, + tcomp); + if (result == 0) + { + // Stop compiler from complaining + this.CheckError(false); + this.FileStream = null; + this.currentFileName = null; + return; + } + } + finally + { + if (namePtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(namePtr); + } + } + + this.CheckError(false); + this.FileStream = null; + this.currentFileName = null; + } + + private void FlushFolder() + { + this.Erf.Clear(); + var result = NativeMethods.FCI.FlushFolder(this.fciHandle, this.fciGetNextCabinet, this.fciCreateStatus); + if (result == 0) + { + // Stop compiler from complaining + this.CheckError(false); + return; + } + this.CheckError(false); + } + + private void FlushCabinet() + { + this.Erf.Clear(); + var result = NativeMethods.FCI.FlushCabinet(this.fciHandle, false, this.fciGetNextCabinet, this.fciCreateStatus); + if (result == 0) + { + // Stop compiler from complaining + this.CheckError(false); + return; + } + this.CheckError(false); + } + + private int CabGetOpenInfo( + string path, + out short date, + out short time, + out short attribs, + out int err, + IntPtr pv) + { + CompressionEngine.DateTimeToDosDateAndTime(this.fileLastWriteTime, out date, out time); + attribs = (short)this.fileAttributes; + + Stream stream = this.FileStream; + this.FileStream = new DuplicateStream(stream); + int streamHandle = this.StreamHandles.AllocHandle(stream); + err = 0; + return streamHandle; + } + + private int CabFilePlaced( + IntPtr pccab, + string filePath, + long fileSize, + int continuation, + IntPtr pv) + { + return 0; + } + + private int CabGetNextCabinet(IntPtr pccab, uint prevCabSize, IntPtr pv) + { + NativeMethods.FCI.CCAB nextCcab = new NativeMethods.FCI.CCAB(); + Marshal.PtrToStructure(pccab, nextCcab); + + nextCcab.szDisk = String.Empty; + nextCcab.szCab = this.context.GetArchiveName(nextCcab.iCab); + this.CabNumbers[nextCcab.szCab] = (short)nextCcab.iCab; + this.NextCabinetName = nextCcab.szCab; + + Marshal.StructureToPtr(nextCcab, pccab, false); + return 1; + } + + private int CabCreateStatus(NativeMethods.FCI.STATUS typeStatus, uint cb1, uint cb2, IntPtr pv) + { + switch (typeStatus) + { + case NativeMethods.FCI.STATUS.FILE: + if (cb2 > 0 && this.currentFileBytesProcessed < this.currentFileTotalBytes) + { + if (this.currentFileBytesProcessed + cb2 > this.currentFileTotalBytes) + { + cb2 = (uint)this.currentFileTotalBytes - (uint)this.currentFileBytesProcessed; + } + this.currentFileBytesProcessed += cb2; + this.fileBytesProcessed += cb2; + + this.OnProgress(ArchiveProgressType.PartialFile); + } + break; + + case NativeMethods.FCI.STATUS.FOLDER: + if (cb1 == 0) + { + this.currentFolderTotalBytes = cb2 - this.totalFolderBytesProcessedInCurrentCab; + this.totalFolderBytesProcessedInCurrentCab = cb2; + } + else if (this.currentFolderTotalBytes == 0) + { + this.OnProgress(ArchiveProgressType.PartialArchive); + } + break; + + case NativeMethods.FCI.STATUS.CABINET: + break; + } + return 0; + } + + private int CabGetTempFile(IntPtr tempNamePtr, int tempNameSize, IntPtr pv) + { + string tempFileName; + if (this.UseTempFiles) + { + tempFileName = Path.GetFileName(Path.GetTempFileName()); + } + else + { + tempFileName = CabPacker.TempStreamName; + } + + byte[] tempNameBytes = Encoding.ASCII.GetBytes(tempFileName); + if (tempNameBytes.Length >= tempNameSize) + { + return -1; + } + + Marshal.Copy(tempNameBytes, 0, tempNamePtr, tempNameBytes.Length); + Marshal.WriteByte(tempNamePtr, tempNameBytes.Length, 0); // null-terminator + return 1; + } + + private int CabDeleteFile(string path, out int err, IntPtr pv) + { + try + { + // Deleting a temp file - don't bother if it is only a memory stream. + if (path != CabPacker.TempStreamName) + { + path = Path.Combine(Path.GetTempPath(), path); + File.Delete(path); + } + } + catch (IOException) + { + // Failure to delete a temp file is not fatal. + } + err = 0; + return 1; + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabUnpacker.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabUnpacker.cs new file mode 100644 index 000000000..b5ee9c20e --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabUnpacker.cs @@ -0,0 +1,601 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Cab +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Runtime.InteropServices; +#if !CORECLR + using System.Security.Permissions; +#endif + using System.Text; + + internal class CabUnpacker : CabWorker + { + private NativeMethods.FDI.Handle fdiHandle; + + // These delegates need to be saved as member variables + // so that they don't get GC'd. + private NativeMethods.FDI.PFNALLOC fdiAllocMemHandler; + private NativeMethods.FDI.PFNFREE fdiFreeMemHandler; + private NativeMethods.FDI.PFNOPEN fdiOpenStreamHandler; + private NativeMethods.FDI.PFNREAD fdiReadStreamHandler; + private NativeMethods.FDI.PFNWRITE fdiWriteStreamHandler; + private NativeMethods.FDI.PFNCLOSE fdiCloseStreamHandler; + private NativeMethods.FDI.PFNSEEK fdiSeekStreamHandler; + + private IUnpackStreamContext context; + + private List fileList; + + private int folderId; + + private Predicate filter; + + [SuppressMessage("Microsoft.Security", "CA2106:SecureAsserts")] + [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")] +#if !CORECLR + [SecurityPermission(SecurityAction.Assert, UnmanagedCode = true)] +#endif + public CabUnpacker(CabEngine cabEngine) + : base(cabEngine) + { + this.fdiAllocMemHandler = this.CabAllocMem; + this.fdiFreeMemHandler = this.CabFreeMem; + this.fdiOpenStreamHandler = this.CabOpenStream; + this.fdiReadStreamHandler = this.CabReadStream; + this.fdiWriteStreamHandler = this.CabWriteStream; + this.fdiCloseStreamHandler = this.CabCloseStream; + this.fdiSeekStreamHandler = this.CabSeekStream; + + this.fdiHandle = NativeMethods.FDI.Create( + this.fdiAllocMemHandler, + this.fdiFreeMemHandler, + this.fdiOpenStreamHandler, + this.fdiReadStreamHandler, + this.fdiWriteStreamHandler, + this.fdiCloseStreamHandler, + this.fdiSeekStreamHandler, + NativeMethods.FDI.CPU_80386, + this.ErfHandle.AddrOfPinnedObject()); + if (this.Erf.Error) + { + int error = this.Erf.Oper; + int errorCode = this.Erf.Type; + this.ErfHandle.Free(); + throw new CabException( + error, + errorCode, + CabException.GetErrorMessage(error, errorCode, true)); + } + } + + [SuppressMessage("Microsoft.Security", "CA2106:SecureAsserts")] +#if !CORECLR + [SecurityPermission(SecurityAction.Assert, UnmanagedCode = true)] +#endif + public bool IsArchive(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + lock (this) + { + short id; + int folderCount, fileCount; + return this.IsCabinet(stream, out id, out folderCount, out fileCount); + } + } + + [SuppressMessage("Microsoft.Security", "CA2106:SecureAsserts")] +#if !CORECLR + [SecurityPermission(SecurityAction.Assert, UnmanagedCode = true)] +#endif + public IList GetFileInfo( + IUnpackStreamContext streamContext, + Predicate fileFilter) + { + if (streamContext == null) + { + throw new ArgumentNullException("streamContext"); + } + + lock (this) + { + this.context = streamContext; + this.filter = fileFilter; + this.NextCabinetName = String.Empty; + this.fileList = new List(); + bool tmpSuppress = this.SuppressProgressEvents; + this.SuppressProgressEvents = true; + try + { + for (short cabNumber = 0; + this.NextCabinetName != null; + cabNumber++) + { + this.Erf.Clear(); + this.CabNumbers[this.NextCabinetName] = cabNumber; + + var result = NativeMethods.FDI.Copy( + this.fdiHandle, + this.NextCabinetName, + String.Empty, + 0, + this.CabListNotify, + IntPtr.Zero, + IntPtr.Zero); + if (result == 0) { + // stop compiler from complaining + this.CheckError(true); + } + this.CheckError(true); + } + + List tmpFileList = this.fileList; + this.fileList = null; + return tmpFileList.AsReadOnly(); + } + finally + { + this.SuppressProgressEvents = tmpSuppress; + + if (this.CabStream != null) + { + this.context.CloseArchiveReadStream( + this.currentArchiveNumber, + this.currentArchiveName, + this.CabStream); + this.CabStream = null; + } + + this.context = null; + } + } + } + + [SuppressMessage("Microsoft.Security", "CA2106:SecureAsserts")] +#if !CORECLR + [SecurityPermission(SecurityAction.Assert, UnmanagedCode = true)] +#endif + public void Unpack( + IUnpackStreamContext streamContext, + Predicate fileFilter) + { + lock (this) + { + IList files = + this.GetFileInfo(streamContext, fileFilter); + + this.ResetProgressData(); + + if (files != null) + { + this.totalFiles = files.Count; + + for (int i = 0; i < files.Count; i++) + { + this.totalFileBytes += files[i].Length; + if (files[i].ArchiveNumber >= this.totalArchives) + { + int totalArchives = files[i].ArchiveNumber + 1; + this.totalArchives = (short) totalArchives; + } + } + } + + this.context = streamContext; + this.fileList = null; + this.NextCabinetName = String.Empty; + this.folderId = -1; + this.currentFileNumber = -1; + + try + { + for (short cabNumber = 0; + this.NextCabinetName != null; + cabNumber++) + { + this.Erf.Clear(); + this.CabNumbers[this.NextCabinetName] = cabNumber; + + var result =NativeMethods.FDI.Copy( + this.fdiHandle, + this.NextCabinetName, + String.Empty, + 0, + this.CabExtractNotify, + IntPtr.Zero, + IntPtr.Zero); + if (result == 0) { + // stop compiler from complaining + this.CheckError(true); + } + this.CheckError(true); + } + } + finally + { + if (this.CabStream != null) + { + this.context.CloseArchiveReadStream( + this.currentArchiveNumber, + this.currentArchiveName, + this.CabStream); + this.CabStream = null; + } + + if (this.FileStream != null) + { + this.context.CloseFileWriteStream(this.currentFileName, this.FileStream, FileAttributes.Normal, DateTime.Now); + this.FileStream = null; + } + + this.context = null; + } + } + } + + internal override int CabOpenStreamEx(string path, int openFlags, int shareMode, out int err, IntPtr pv) + { + if (this.CabNumbers.ContainsKey(path)) + { + Stream stream = this.CabStream; + if (stream == null) + { + short cabNumber = this.CabNumbers[path]; + + stream = this.context.OpenArchiveReadStream(cabNumber, path, this.CabEngine); + if (stream == null) + { + throw new FileNotFoundException(String.Format(CultureInfo.InvariantCulture, "Cabinet {0} not provided.", cabNumber)); + } + this.currentArchiveName = path; + this.currentArchiveNumber = cabNumber; + if (this.totalArchives <= this.currentArchiveNumber) + { + int totalArchives = this.currentArchiveNumber + 1; + this.totalArchives = (short) totalArchives; + } + this.currentArchiveTotalBytes = stream.Length; + this.currentArchiveBytesProcessed = 0; + + if (this.folderId != -3) // -3 is a special folderId that requires re-opening the same cab + { + this.OnProgress(ArchiveProgressType.StartArchive); + } + this.CabStream = stream; + } + path = CabWorker.CabStreamName; + } + return base.CabOpenStreamEx(path, openFlags, shareMode, out err, pv); + } + + internal override int CabReadStreamEx(int streamHandle, IntPtr memory, int cb, out int err, IntPtr pv) + { + int count = base.CabReadStreamEx(streamHandle, memory, cb, out err, pv); + if (err == 0 && this.CabStream != null) + { + if (this.fileList == null) + { + Stream stream = this.StreamHandles[streamHandle]; + if (DuplicateStream.OriginalStream(stream) == + DuplicateStream.OriginalStream(this.CabStream)) + { + this.currentArchiveBytesProcessed += cb; + if (this.currentArchiveBytesProcessed > this.currentArchiveTotalBytes) + { + this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes; + } + } + } + } + return count; + } + + internal override int CabWriteStreamEx(int streamHandle, IntPtr memory, int cb, out int err, IntPtr pv) + { + int count = base.CabWriteStreamEx(streamHandle, memory, cb, out err, pv); + if (count > 0 && err == 0) + { + this.currentFileBytesProcessed += cb; + this.fileBytesProcessed += cb; + this.OnProgress(ArchiveProgressType.PartialFile); + } + return count; + } + + internal override int CabCloseStreamEx(int streamHandle, out int err, IntPtr pv) + { + Stream stream = DuplicateStream.OriginalStream(this.StreamHandles[streamHandle]); + + if (stream == DuplicateStream.OriginalStream(this.CabStream)) + { + if (this.folderId != -3) // -3 is a special folderId that requires re-opening the same cab + { + this.OnProgress(ArchiveProgressType.FinishArchive); + } + + this.context.CloseArchiveReadStream(this.currentArchiveNumber, this.currentArchiveName, stream); + + this.currentArchiveName = this.NextCabinetName; + this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes = 0; + + this.CabStream = null; + } + return base.CabCloseStreamEx(streamHandle, out err, pv); + } + + /// + /// Disposes of resources allocated by the cabinet engine. + /// + /// If true, the method has been called directly or indirectly by a user's code, + /// so managed and unmanaged resources will be disposed. If false, the method has been called by the + /// runtime from inside the finalizer, and only unmanaged resources will be disposed. + [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")] + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (this.fdiHandle != null) + { + this.fdiHandle.Dispose(); + this.fdiHandle = null; + } + } + } + finally + { + base.Dispose(disposing); + } + } + + private static string GetFileName(NativeMethods.FDI.NOTIFICATION notification) + { + bool utf8Name = (notification.attribs & (ushort) FileAttributes.Normal) != 0; // _A_NAME_IS_UTF + + // Non-utf8 names should be completely ASCII. But for compatibility with + // legacy tools, interpret them using the current (Default) ANSI codepage. + Encoding nameEncoding = utf8Name ? Encoding.UTF8 : Encoding.GetEncoding(0); + + // Find how many bytes are in the string. + // Unfortunately there is no faster way. + int nameBytesCount = 0; + while (Marshal.ReadByte(notification.psz1, nameBytesCount) != 0) + { + nameBytesCount++; + } + + byte[] nameBytes = new byte[nameBytesCount]; + Marshal.Copy(notification.psz1, nameBytes, 0, nameBytesCount); + string name = nameEncoding.GetString(nameBytes); + if (Path.IsPathRooted(name)) + { + name = name.Replace("" + Path.VolumeSeparatorChar, ""); + } + + return name; + } + + private bool IsCabinet(Stream cabStream, out short id, out int cabFolderCount, out int fileCount) + { + int streamHandle = this.StreamHandles.AllocHandle(cabStream); + try + { + this.Erf.Clear(); + NativeMethods.FDI.CABINFO fdici; + bool isCabinet = 0 != NativeMethods.FDI.IsCabinet(this.fdiHandle, streamHandle, out fdici); + + if (this.Erf.Error) + { + if (((NativeMethods.FDI.ERROR) this.Erf.Oper) == NativeMethods.FDI.ERROR.UNKNOWN_CABINET_VERSION) + { + isCabinet = false; + } + else + { + throw new CabException( + this.Erf.Oper, + this.Erf.Type, + CabException.GetErrorMessage(this.Erf.Oper, this.Erf.Type, true)); + } + } + + id = fdici.setID; + cabFolderCount = (int) fdici.cFolders; + fileCount = (int) fdici.cFiles; + return isCabinet; + } + finally + { + this.StreamHandles.FreeHandle(streamHandle); + } + } + + private int CabListNotify(NativeMethods.FDI.NOTIFICATIONTYPE notificationType, NativeMethods.FDI.NOTIFICATION notification) + { + switch (notificationType) + { + case NativeMethods.FDI.NOTIFICATIONTYPE.CABINET_INFO: + { + string nextCab = Marshal.PtrToStringAnsi(notification.psz1); + this.NextCabinetName = (nextCab.Length != 0 ? nextCab : null); + return 0; // Continue + } + case NativeMethods.FDI.NOTIFICATIONTYPE.PARTIAL_FILE: + { + // This notification can occur when examining the contents of a non-first cab file. + return 0; // Continue + } + case NativeMethods.FDI.NOTIFICATIONTYPE.COPY_FILE: + { + //bool execute = (notification.attribs & (ushort) FileAttributes.Device) != 0; // _A_EXEC + + string name = CabUnpacker.GetFileName(notification); + + if (this.filter == null || this.filter(name)) + { + if (this.fileList != null) + { + FileAttributes attributes = (FileAttributes) notification.attribs & + (FileAttributes.Archive | FileAttributes.Hidden | FileAttributes.ReadOnly | FileAttributes.System); + if (attributes == (FileAttributes) 0) + { + attributes = FileAttributes.Normal; + } + DateTime lastWriteTime; + CompressionEngine.DosDateAndTimeToDateTime(notification.date, notification.time, out lastWriteTime); + long length = notification.cb; + + CabFileInfo fileInfo = new CabFileInfo( + name, + notification.iFolder, + notification.iCabinet, + attributes, + lastWriteTime, + length); + this.fileList.Add(fileInfo); + this.currentFileNumber = this.fileList.Count - 1; + this.fileBytesProcessed += notification.cb; + } + } + + this.totalFiles++; + this.totalFileBytes += notification.cb; + return 0; // Continue + } + } + return 0; + } + + private int CabExtractNotify(NativeMethods.FDI.NOTIFICATIONTYPE notificationType, NativeMethods.FDI.NOTIFICATION notification) + { + switch (notificationType) + { + case NativeMethods.FDI.NOTIFICATIONTYPE.CABINET_INFO: + { + if (this.NextCabinetName != null && this.NextCabinetName.StartsWith("?", StringComparison.Ordinal)) + { + // We are just continuing the copy of a file that spanned cabinets. + // The next cabinet name needs to be preserved. + this.NextCabinetName = this.NextCabinetName.Substring(1); + } + else + { + string nextCab = Marshal.PtrToStringAnsi(notification.psz1); + this.NextCabinetName = (nextCab.Length != 0 ? nextCab : null); + } + return 0; // Continue + } + case NativeMethods.FDI.NOTIFICATIONTYPE.NEXT_CABINET: + { + string nextCab = Marshal.PtrToStringAnsi(notification.psz1); + this.CabNumbers[nextCab] = (short) notification.iCabinet; + this.NextCabinetName = "?" + this.NextCabinetName; + return 0; // Continue + } + case NativeMethods.FDI.NOTIFICATIONTYPE.COPY_FILE: + { + return this.CabExtractCopyFile(notification); + } + case NativeMethods.FDI.NOTIFICATIONTYPE.CLOSE_FILE_INFO: + { + return this.CabExtractCloseFile(notification); + } + } + return 0; + } + + private int CabExtractCopyFile(NativeMethods.FDI.NOTIFICATION notification) + { + if (notification.iFolder != this.folderId) + { + if (notification.iFolder != -3) // -3 is a special folderId used when continuing a folder from a previous cab + { + if (this.folderId != -1) // -1 means we just started the extraction sequence + { + this.currentFolderNumber++; + } + } + this.folderId = notification.iFolder; + } + + //bool execute = (notification.attribs & (ushort) FileAttributes.Device) != 0; // _A_EXEC + + string name = CabUnpacker.GetFileName(notification); + + if (this.filter == null || this.filter(name)) + { + this.currentFileNumber++; + this.currentFileName = name; + + this.currentFileBytesProcessed = 0; + this.currentFileTotalBytes = notification.cb; + this.OnProgress(ArchiveProgressType.StartFile); + + DateTime lastWriteTime; + CompressionEngine.DosDateAndTimeToDateTime(notification.date, notification.time, out lastWriteTime); + + Stream stream = this.context.OpenFileWriteStream(name, notification.cb, lastWriteTime); + if (stream != null) + { + this.FileStream = stream; + int streamHandle = this.StreamHandles.AllocHandle(stream); + return streamHandle; + } + else + { + this.fileBytesProcessed += notification.cb; + this.OnProgress(ArchiveProgressType.FinishFile); + this.currentFileName = null; + } + } + return 0; // Continue + } + + private int CabExtractCloseFile(NativeMethods.FDI.NOTIFICATION notification) + { + Stream stream = this.StreamHandles[notification.hf]; + this.StreamHandles.FreeHandle(notification.hf); + + //bool execute = (notification.attribs & (ushort) FileAttributes.Device) != 0; // _A_EXEC + + string name = CabUnpacker.GetFileName(notification); + + FileAttributes attributes = (FileAttributes) notification.attribs & + (FileAttributes.Archive | FileAttributes.Hidden | FileAttributes.ReadOnly | FileAttributes.System); + if (attributes == (FileAttributes) 0) + { + attributes = FileAttributes.Normal; + } + DateTime lastWriteTime; + CompressionEngine.DosDateAndTimeToDateTime(notification.date, notification.time, out lastWriteTime); + + stream.Flush(); + this.context.CloseFileWriteStream(name, stream, attributes, lastWriteTime); + this.FileStream = null; + + long remainder = this.currentFileTotalBytes - this.currentFileBytesProcessed; + this.currentFileBytesProcessed += remainder; + this.fileBytesProcessed += remainder; + this.OnProgress(ArchiveProgressType.FinishFile); + this.currentFileName = null; + + return 1; // Continue + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabWorker.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabWorker.cs new file mode 100644 index 000000000..4d0250451 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/CabWorker.cs @@ -0,0 +1,349 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Cab +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Runtime.InteropServices; + + internal abstract class CabWorker : IDisposable + { + internal const string CabStreamName = "%%CAB%%"; + + private CabEngine cabEngine; + + private HandleManager streamHandles; + private Stream cabStream; + private Stream fileStream; + + private NativeMethods.ERF erf; + private GCHandle erfHandle; + + private IDictionary cabNumbers; + private string nextCabinetName; + + private bool suppressProgressEvents; + + private byte[] buf; + + // Progress data + protected string currentFileName; + protected int currentFileNumber; + protected int totalFiles; + protected long currentFileBytesProcessed; + protected long currentFileTotalBytes; + protected short currentFolderNumber; + protected long currentFolderTotalBytes; + protected string currentArchiveName; + protected short currentArchiveNumber; + protected short totalArchives; + protected long currentArchiveBytesProcessed; + protected long currentArchiveTotalBytes; + protected long fileBytesProcessed; + protected long totalFileBytes; + + [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")] + protected CabWorker(CabEngine cabEngine) + { + this.cabEngine = cabEngine; + this.streamHandles = new HandleManager(); + this.erf = new NativeMethods.ERF(); + this.erfHandle = GCHandle.Alloc(this.erf, GCHandleType.Pinned); + this.cabNumbers = new Dictionary(1); + + // 32K seems to be the size of the largest chunks processed by cabinet.dll. + // But just in case, this buffer will auto-enlarge. + this.buf = new byte[32768]; + } + + ~CabWorker() + { + this.Dispose(false); + } + + public CabEngine CabEngine + { + get + { + return this.cabEngine; + } + } + + internal NativeMethods.ERF Erf + { + get + { + return this.erf; + } + } + + internal GCHandle ErfHandle + { + get + { + return this.erfHandle; + } + } + + internal HandleManager StreamHandles + { + get + { + return this.streamHandles; + } + } + + internal bool SuppressProgressEvents + { + get + { + return this.suppressProgressEvents; + } + + set + { + this.suppressProgressEvents = value; + } + } + + internal IDictionary CabNumbers + { + get + { + return this.cabNumbers; + } + } + + internal string NextCabinetName + { + get + { + return this.nextCabinetName; + } + + set + { + this.nextCabinetName = value; + } + } + + internal Stream CabStream + { + get + { + return this.cabStream; + } + + set + { + this.cabStream = value; + } + } + + internal Stream FileStream + { + get + { + return this.fileStream; + } + + set + { + this.fileStream = value; + } + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected void ResetProgressData() + { + this.currentFileName = null; + this.currentFileNumber = 0; + this.totalFiles = 0; + this.currentFileBytesProcessed = 0; + this.currentFileTotalBytes = 0; + this.currentFolderNumber = 0; + this.currentFolderTotalBytes = 0; + this.currentArchiveName = null; + this.currentArchiveNumber = 0; + this.totalArchives = 0; + this.currentArchiveBytesProcessed = 0; + this.currentArchiveTotalBytes = 0; + this.fileBytesProcessed = 0; + this.totalFileBytes = 0; + } + + protected void OnProgress(ArchiveProgressType progressType) + { + if (!this.suppressProgressEvents) + { + ArchiveProgressEventArgs e = new ArchiveProgressEventArgs( + progressType, + this.currentFileName, + this.currentFileNumber >= 0 ? this.currentFileNumber : 0, + this.totalFiles, + this.currentFileBytesProcessed, + this.currentFileTotalBytes, + this.currentArchiveName, + this.currentArchiveNumber, + this.totalArchives, + this.currentArchiveBytesProcessed, + this.currentArchiveTotalBytes, + this.fileBytesProcessed, + this.totalFileBytes); + this.CabEngine.ReportProgress(e); + } + } + + internal IntPtr CabAllocMem(int byteCount) + { + IntPtr memPointer = Marshal.AllocHGlobal((IntPtr) byteCount); + return memPointer; + } + + internal void CabFreeMem(IntPtr memPointer) + { + Marshal.FreeHGlobal(memPointer); + } + + internal int CabOpenStream(string path, int openFlags, int shareMode) + { + int err; return this.CabOpenStreamEx(path, openFlags, shareMode, out err, IntPtr.Zero); + } + + internal virtual int CabOpenStreamEx(string path, int openFlags, int shareMode, out int err, IntPtr pv) + { + path = path.Trim(); + Stream stream = this.cabStream; + this.cabStream = new DuplicateStream(stream); + int streamHandle = this.streamHandles.AllocHandle(stream); + err = 0; + return streamHandle; + } + + internal int CabReadStream(int streamHandle, IntPtr memory, int cb) + { + int err; return this.CabReadStreamEx(streamHandle, memory, cb, out err, IntPtr.Zero); + } + + internal virtual int CabReadStreamEx(int streamHandle, IntPtr memory, int cb, out int err, IntPtr pv) + { + Stream stream = this.streamHandles[streamHandle]; + int count = (int) cb; + if (count > this.buf.Length) + { + this.buf = new byte[count]; + } + count = stream.Read(this.buf, 0, count); + Marshal.Copy(this.buf, 0, memory, count); + err = 0; + return count; + } + + internal int CabWriteStream(int streamHandle, IntPtr memory, int cb) + { + int err; return this.CabWriteStreamEx(streamHandle, memory, cb, out err, IntPtr.Zero); + } + + internal virtual int CabWriteStreamEx(int streamHandle, IntPtr memory, int cb, out int err, IntPtr pv) + { + Stream stream = this.streamHandles[streamHandle]; + int count = (int) cb; + if (count > this.buf.Length) + { + this.buf = new byte[count]; + } + Marshal.Copy(memory, this.buf, 0, count); + stream.Write(this.buf, 0, count); + err = 0; + return cb; + } + + internal int CabCloseStream(int streamHandle) + { + int err; return this.CabCloseStreamEx(streamHandle, out err, IntPtr.Zero); + } + + internal virtual int CabCloseStreamEx(int streamHandle, out int err, IntPtr pv) + { + this.streamHandles.FreeHandle(streamHandle); + err = 0; + return 0; + } + + internal int CabSeekStream(int streamHandle, int offset, int seekOrigin) + { + int err; return this.CabSeekStreamEx(streamHandle, offset, seekOrigin, out err, IntPtr.Zero); + } + + internal virtual int CabSeekStreamEx(int streamHandle, int offset, int seekOrigin, out int err, IntPtr pv) + { + Stream stream = this.streamHandles[streamHandle]; + offset = (int) stream.Seek(offset, (SeekOrigin) seekOrigin); + err = 0; + return offset; + } + + /// + /// Disposes of resources allocated by the cabinet engine. + /// + /// If true, the method has been called directly or indirectly by a user's code, + /// so managed and unmanaged resources will be disposed. If false, the method has been called by the + /// runtime from inside the finalizer, and only unmanaged resources will be disposed. + [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")] + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (this.cabStream != null) + { +#if CORECLR + this.cabStream.Dispose(); +#else + this.cabStream.Close(); +#endif + this.cabStream = null; + } + + if (this.fileStream != null) + { +#if CORECLR + this.fileStream.Dispose(); +#else + this.fileStream.Close(); +#endif + this.fileStream = null; + } + } + + if (this.erfHandle.IsAllocated) + { + this.erfHandle.Free(); + } + } + + protected void CheckError(bool extracting) + { + if (this.Erf.Error) + { + throw new CabException( + this.Erf.Oper, + this.Erf.Type, + CabException.GetErrorMessage(this.Erf.Oper, this.Erf.Type, extracting)); + } + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/HandleManager.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/HandleManager.cs new file mode 100644 index 000000000..a8aaa3735 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/HandleManager.cs @@ -0,0 +1,82 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Cab +{ + using System.Collections.Generic; + + /// + /// Generic class for managing allocations of integer handles + /// for objects of a certain type. + /// + /// The type of objects the handles refer to. + internal sealed class HandleManager where T : class + { + /// + /// Auto-resizing list of objects for which handles have been allocated. + /// Each handle is just an index into this list. When a handle is freed, + /// the list item at that index is set to null. + /// + private List handles; + + /// + /// Creates a new HandleManager instance. + /// + public HandleManager() + { + this.handles = new List(); + } + + /// + /// Gets the object of a handle, or null if the handle is invalid. + /// + /// The integer handle previously allocated + /// for the desired object. + /// The object for which the handle was allocated. + public T this[int handle] + { + get + { + if (handle > 0 && handle <= this.handles.Count) + { + return this.handles[handle - 1]; + } + else + { + return null; + } + } + } + + /// + /// Allocates a new handle for an object. + /// + /// Object that the handle will refer to. + /// New handle that can be later used to retrieve the object. + public int AllocHandle(T obj) + { + this.handles.Add(obj); + int handle = this.handles.Count; + return handle; + } + + /// + /// Frees a handle that was previously allocated. Afterward the handle + /// will be invalid and the object it referred to can no longer retrieved. + /// + /// Handle to be freed. + public void FreeHandle(int handle) + { + if (handle > 0 && handle <= this.handles.Count) + { + this.handles[handle - 1] = null; + } + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/NativeMethods.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/NativeMethods.cs new file mode 100644 index 000000000..5e16efa77 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Cab/NativeMethods.cs @@ -0,0 +1,423 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Cab +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Runtime.InteropServices; + using System.Security; +#if !CORECLR + using System.Security.Permissions; +#endif + + /// + /// Native DllImport methods and related structures and constants used for + /// cabinet creation and extraction via cabinet.dll. + /// + internal static class NativeMethods + { + /// + /// A direct import of constants, enums, structures, delegates, and functions from fci.h. + /// Refer to comments in fci.h for documentation. + /// + internal static class FCI + { + internal const int MIN_DISK = 32768; + internal const int MAX_DISK = Int32.MaxValue; + internal const int MAX_FOLDER = 0x7FFF8000; + internal const int MAX_FILENAME = 256; + internal const int MAX_CABINET_NAME = 256; + internal const int MAX_CAB_PATH = 256; + internal const int MAX_DISK_NAME = 256; + + internal const int CPU_80386 = 1; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate IntPtr PFNALLOC(int cb); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void PFNFREE(IntPtr pv); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNOPEN(string path, int oflag, int pmode, out int err, IntPtr pv); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNREAD(int fileHandle, IntPtr memory, int cb, out int err, IntPtr pv); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNWRITE(int fileHandle, IntPtr memory, int cb, out int err, IntPtr pv); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNCLOSE(int fileHandle, out int err, IntPtr pv); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNSEEK(int fileHandle, int dist, int seekType, out int err, IntPtr pv); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNDELETE(string path, out int err, IntPtr pv); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNGETNEXTCABINET(IntPtr pccab, uint cbPrevCab, IntPtr pv); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNFILEPLACED(IntPtr pccab, string path, long fileSize, int continuation, IntPtr pv); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNGETOPENINFO(string path, out short date, out short time, out short pattribs, out int err, IntPtr pv); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNSTATUS(STATUS typeStatus, uint cb1, uint cb2, IntPtr pv); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNGETTEMPFILE(IntPtr tempNamePtr, int tempNameSize, IntPtr pv); + + /// + /// Error codes that can be returned by FCI. + /// + internal enum ERROR : int + { + NONE, + OPEN_SRC, + READ_SRC, + ALLOC_FAIL, + TEMP_FILE, + BAD_COMPR_TYPE, + CAB_FILE, + USER_ABORT, + MCI_FAIL, + } + + /// + /// FCI compression algorithm types and parameters. + /// + internal enum TCOMP : ushort + { + MASK_TYPE = 0x000F, + TYPE_NONE = 0x0000, + TYPE_MSZIP = 0x0001, + TYPE_QUANTUM = 0x0002, + TYPE_LZX = 0x0003, + BAD = 0x000F, + + MASK_LZX_WINDOW = 0x1F00, + LZX_WINDOW_LO = 0x0F00, + LZX_WINDOW_HI = 0x1500, + SHIFT_LZX_WINDOW = 0x0008, + + MASK_QUANTUM_LEVEL = 0x00F0, + QUANTUM_LEVEL_LO = 0x0010, + QUANTUM_LEVEL_HI = 0x0070, + SHIFT_QUANTUM_LEVEL = 0x0004, + + MASK_QUANTUM_MEM = 0x1F00, + QUANTUM_MEM_LO = 0x0A00, + QUANTUM_MEM_HI = 0x1500, + SHIFT_QUANTUM_MEM = 0x0008, + + MASK_RESERVED = 0xE000, + } + + /// + /// Reason for FCI status callback. + /// + internal enum STATUS : uint + { + FILE = 0, + FOLDER = 1, + CABINET = 2, + } + + [SuppressMessage("Microsoft.Globalization", "CA2101:SpecifyMarshalingForPInvokeStringArguments")] + [DllImport("cabinet.dll", EntryPoint = "FCICreate", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)] + internal static extern Handle Create(IntPtr perf, PFNFILEPLACED pfnfcifp, PFNALLOC pfna, PFNFREE pfnf, PFNOPEN pfnopen, PFNREAD pfnread, PFNWRITE pfnwrite, PFNCLOSE pfnclose, PFNSEEK pfnseek, PFNDELETE pfndelete, PFNGETTEMPFILE pfnfcigtf, [MarshalAs(UnmanagedType.LPStruct)] CCAB pccab, IntPtr pv); + + [DllImport("cabinet.dll", EntryPoint = "FCIAddFile", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)] + internal static extern int AddFile(Handle hfci, string pszSourceFile, IntPtr pszFileName, [MarshalAs(UnmanagedType.Bool)] bool fExecute, PFNGETNEXTCABINET pfnfcignc, PFNSTATUS pfnfcis, PFNGETOPENINFO pfnfcigoi, TCOMP typeCompress); + + [DllImport("cabinet.dll", EntryPoint = "FCIFlushCabinet", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)] + internal static extern int FlushCabinet(Handle hfci, [MarshalAs(UnmanagedType.Bool)] bool fGetNextCab, PFNGETNEXTCABINET pfnfcignc, PFNSTATUS pfnfcis); + + [DllImport("cabinet.dll", EntryPoint = "FCIFlushFolder", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)] + internal static extern int FlushFolder(Handle hfci, PFNGETNEXTCABINET pfnfcignc, PFNSTATUS pfnfcis); + +#if !CORECLR + [SuppressUnmanagedCodeSecurity] +#endif + [DllImport("cabinet.dll", EntryPoint = "FCIDestroy", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Destroy(IntPtr hfci); + + /// + /// Cabinet information structure used for FCI initialization and GetNextCabinet callback. + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + internal class CCAB + { + internal int cb = MAX_DISK; + internal int cbFolderThresh = MAX_FOLDER; + internal int cbReserveCFHeader; + internal int cbReserveCFFolder; + internal int cbReserveCFData; + internal int iCab; + internal int iDisk; + internal int fFailOnIncompressible; + internal short setID; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_DISK_NAME)] internal string szDisk = String.Empty; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_CABINET_NAME)] internal string szCab = String.Empty; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_CAB_PATH)] internal string szCabPath = String.Empty; + } + + /// + /// Ensures that the FCI handle is safely released. + /// + internal class Handle : SafeHandle + { + /// + /// Creates a new unintialized handle. The handle will be initialized + /// when it is marshalled back from native code. + /// + internal Handle() + : base(IntPtr.Zero, true) + { + } + + /// + /// Checks if the handle is invalid. An FCI handle is invalid when it is zero. + /// + public override bool IsInvalid + { + get + { + return this.handle == IntPtr.Zero; + } + } + + /// + /// Releases the handle by calling FDIDestroy(). + /// + /// True if the release succeeded. +#if !CORECLR + [SecurityPermission(SecurityAction.Assert, UnmanagedCode = true)] +#endif + protected override bool ReleaseHandle() + { + return FCI.Destroy(this.handle); + } + } + } + + /// + /// A direct import of constants, enums, structures, delegates, and functions from fdi.h. + /// Refer to comments in fdi.h for documentation. + /// + internal static class FDI + { + internal const int MAX_DISK = Int32.MaxValue; + internal const int MAX_FILENAME = 256; + internal const int MAX_CABINET_NAME = 256; + internal const int MAX_CAB_PATH = 256; + internal const int MAX_DISK_NAME = 256; + + internal const int CPU_80386 = 1; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate IntPtr PFNALLOC(int cb); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void PFNFREE(IntPtr pv); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNOPEN(string path, int oflag, int pmode); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNREAD(int hf, IntPtr pv, int cb); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNWRITE(int hf, IntPtr pv, int cb); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNCLOSE(int hf); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNSEEK(int hf, int dist, int seektype); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int PFNNOTIFY(NOTIFICATIONTYPE fdint, NOTIFICATION fdin); + + /// + /// Error codes that can be returned by FDI. + /// + internal enum ERROR : int + { + NONE, + CABINET_NOT_FOUND, + NOT_A_CABINET, + UNKNOWN_CABINET_VERSION, + CORRUPT_CABINET, + ALLOC_FAIL, + BAD_COMPR_TYPE, + MDI_FAIL, + TARGET_FILE, + RESERVE_MISMATCH, + WRONG_CABINET, + USER_ABORT, + } + + /// + /// Type of notification message for the FDI Notify callback. + /// + internal enum NOTIFICATIONTYPE : int + { + CABINET_INFO, + PARTIAL_FILE, + COPY_FILE, + CLOSE_FILE_INFO, + NEXT_CABINET, + ENUMERATE, + } + + [DllImport("cabinet.dll", EntryPoint = "FDICreate", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)] + internal static extern Handle Create([MarshalAs(UnmanagedType.FunctionPtr)] PFNALLOC pfnalloc, [MarshalAs(UnmanagedType.FunctionPtr)] PFNFREE pfnfree, PFNOPEN pfnopen, PFNREAD pfnread, PFNWRITE pfnwrite, PFNCLOSE pfnclose, PFNSEEK pfnseek, int cpuType, IntPtr perf); + + [DllImport("cabinet.dll", EntryPoint = "FDICopy", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)] + internal static extern int Copy(Handle hfdi, string pszCabinet, string pszCabPath, int flags, PFNNOTIFY pfnfdin, IntPtr pfnfdid, IntPtr pvUser); + +#if !CORECLR + [SuppressUnmanagedCodeSecurity] +#endif + [DllImport("cabinet.dll", EntryPoint = "FDIDestroy", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool Destroy(IntPtr hfdi); + + [DllImport("cabinet.dll", EntryPoint = "FDIIsCabinet", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true, CallingConvention = CallingConvention.Cdecl)] + [SuppressMessage("Microsoft.Portability", "CA1901:PInvokeDeclarationsShouldBePortable", Justification = "FDI file handles definitely remain 4 bytes on 64bit platforms.")] + internal static extern int IsCabinet(Handle hfdi, int hf, out CABINFO pfdici); + + /// + /// Cabinet information structure filled in by FDI IsCabinet. + /// + [StructLayout(LayoutKind.Sequential)] + internal struct CABINFO + { + internal int cbCabinet; + internal short cFolders; + internal short cFiles; + internal short setID; + internal short iCabinet; + internal int fReserve; + internal int hasprev; + internal int hasnext; + } + + /// + /// Cabinet notification details passed to the FDI Notify callback. + /// + [SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses")] + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + internal class NOTIFICATION + { + internal int cb; + internal IntPtr psz1; + internal IntPtr psz2; + internal IntPtr psz3; + internal IntPtr pv; + + internal IntPtr hf_ptr; + + internal short date; + internal short time; + internal short attribs; + internal short setID; + internal short iCabinet; + internal short iFolder; + internal int fdie; + + // Unlike all the other file handles in FCI/FDI, this one is + // actually pointer-sized. Use a property to pretend it isn't. + internal int hf + { + get { return (int)this.hf_ptr; } + } + } + + /// + /// Ensures that the FDI handle is safely released. + /// + internal class Handle : SafeHandle + { + /// + /// Creates a new unintialized handle. The handle will be initialized + /// when it is marshalled back from native code. + /// + internal Handle() + : base(IntPtr.Zero, true) + { + } + + /// + /// Checks if the handle is invalid. An FDI handle is invalid when it is zero. + /// + public override bool IsInvalid + { + get + { + return this.handle == IntPtr.Zero; + } + } + + /// + /// Releases the handle by calling FDIDestroy(). + /// + /// True if the release succeeded. + protected override bool ReleaseHandle() + { + return FDI.Destroy(this.handle); + } + } + } + + /// + /// Error info structure for FCI and FDI. + /// + /// Before being passed to FCI or FDI, this structure is + /// pinned in memory via a GCHandle. The pinning is necessary + /// to be able to read the results, since the ERF structure doesn't + /// get marshalled back out after an error. + [StructLayout(LayoutKind.Sequential)] + internal class ERF + { + private int erfOper; + private int erfType; + private int fError; + + /// + /// Gets or sets the cabinet error code. + /// + internal int Oper + { + get + { + return this.erfOper; + } + + set + { + this.erfOper = value; + } + } + + /// + /// Gets or sets the Win32 error code. + /// + internal int Type + { + get + { + return this.erfType; + } + + set + { + this.erfType = value; + } + } + + /// + /// GCHandle doesn't like the bool type, so use an int underneath. + /// + internal bool Error + { + get + { + return this.fError != 0; + } + + set + { + this.fError = value ? 1 : 0; + } + } + + /// + /// Clears the error information. + /// + internal void Clear() + { + this.Oper = 0; + this.Type = 0; + this.Error = false; + } + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/CargoStream.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/CargoStream.cs new file mode 100644 index 000000000..3d314f15d --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/CargoStream.cs @@ -0,0 +1,219 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression +{ + using System; + using System.Collections.Generic; + using System.IO; + + /// + /// Wraps a source stream and carries additional items that are disposed when the stream is closed. + /// + public class CargoStream : Stream + { + private Stream source; + private List cargo; + + /// + /// Creates a new a cargo stream. + /// + /// source of the stream + /// List of additional items that are disposed when the stream is closed. + /// The order of the list is the order in which the items are disposed. + public CargoStream(Stream source, params IDisposable[] cargo) + { + if (source == null) + { + throw new ArgumentNullException("source"); + } + + this.source = source; + this.cargo = new List(cargo); + } + + /// + /// Gets the source stream of the cargo stream. + /// + public Stream Source + { + get + { + return this.source; + } + } + + /// + /// Gets the list of additional items that are disposed when the stream is closed. + /// The order of the list is the order in which the items are disposed. The contents can be modified any time. + /// + public IList Cargo + { + get + { + return this.cargo; + } + } + + /// + /// Gets a value indicating whether the source stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead + { + get + { + return this.source.CanRead; + } + } + + /// + /// Gets a value indicating whether the source stream supports writing. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite + { + get + { + return this.source.CanWrite; + } + } + + /// + /// Gets a value indicating whether the source stream supports seeking. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek + { + get + { + return this.source.CanSeek; + } + } + + /// + /// Gets the length of the source stream. + /// + public override long Length + { + get + { + return this.source.Length; + } + } + + /// + /// Gets or sets the position of the source stream. + /// + public override long Position + { + get + { + return this.source.Position; + } + + set + { + this.source.Position = value; + } + } + + /// + /// Flushes the source stream. + /// + public override void Flush() + { + this.source.Flush(); + } + + /// + /// Sets the length of the source stream. + /// + /// The desired length of the stream in bytes. + public override void SetLength(long value) + { + this.source.SetLength(value); + } + +#if !CORECLR + /// + /// Closes the source stream and also closes the additional objects that are carried. + /// + public override void Close() + { + this.source.Close(); + + foreach (IDisposable cargoObject in this.cargo) + { + cargoObject.Dispose(); + } + } +#endif + + /// + /// Disposes source stream and additional objects carried + /// + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.source.Dispose(); + + foreach (IDisposable cargoObject in this.cargo) + { + cargoObject.Dispose(); + } + + } + } + + /// + /// Reads from the source stream. + /// + /// An array of bytes. When this method returns, the buffer + /// contains the specified byte array with the values between offset and + /// (offset + count - 1) replaced by the bytes read from the source. + /// The zero-based byte offset in buffer at which to begin + /// storing the data read from the stream. + /// The maximum number of bytes to be read from the stream. + /// The total number of bytes read into the buffer. This can be less + /// than the number of bytes requested if that many bytes are not currently available, + /// or zero (0) if the end of the stream has been reached. + public override int Read(byte[] buffer, int offset, int count) + { + return this.source.Read(buffer, offset, count); + } + + /// + /// Writes to the source stream. + /// + /// An array of bytes. This method copies count + /// bytes from buffer to the stream. + /// The zero-based byte offset in buffer at which + /// to begin copying bytes to the stream. + /// The number of bytes to be written to the stream. + public override void Write(byte[] buffer, int offset, int count) + { + this.source.Write(buffer, offset, count); + } + + /// + /// Changes the position of the source stream. + /// + /// A byte offset relative to the origin parameter. + /// A value of type SeekOrigin indicating the reference + /// point used to obtain the new position. + /// The new position within the stream. + public override long Seek(long offset, SeekOrigin origin) + { + return this.source.Seek(offset, origin); + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/CompressionEngine.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/CompressionEngine.cs new file mode 100644 index 000000000..5e96f86af --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/CompressionEngine.cs @@ -0,0 +1,377 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression +{ + using System; + using System.Collections.Generic; + using System.IO; + + /// + /// Base class for an engine capable of packing and unpacking a particular + /// compressed file format. + /// + public abstract class CompressionEngine : IDisposable + { + private CompressionLevel compressionLevel; + private bool dontUseTempFiles; + + /// + /// Creates a new instance of the compression engine base class. + /// + protected CompressionEngine() + { + this.compressionLevel = CompressionLevel.Normal; + } + + /// + /// Disposes the compression engine. + /// + ~CompressionEngine() + { + this.Dispose(false); + } + + /// + /// Occurs when the compression engine reports progress in packing + /// or unpacking an archive. + /// + /// + public event EventHandler Progress; + + /// + /// Gets or sets a flag indicating whether temporary files are created + /// and used during compression. + /// + /// True if temporary files are used; false if compression is done + /// entirely in-memory. + /// The value of this property is true by default. Using temporary + /// files can greatly reduce the memory requirement of compression, + /// especially when compressing large archives. However, setting this property + /// to false may yield slightly better performance when creating small + /// archives. Or it may be necessary if the process does not have sufficient + /// privileges to create temporary files. + public bool UseTempFiles + { + get + { + return !this.dontUseTempFiles; + } + + set + { + this.dontUseTempFiles = !value; + } + } + + /// + /// Compression level to use when compressing files. + /// + /// A compression level ranging from minimum to maximum compression, + /// or no compression. + public CompressionLevel CompressionLevel + { + get + { + return this.compressionLevel; + } + + set + { + this.compressionLevel = value; + } + } + + /// + /// Disposes of resources allocated by the compression engine. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Creates an archive. + /// + /// A context interface to handle opening + /// and closing of archive and file streams. + /// The paths of the files in the archive + /// (not external file paths). + /// The archive could not be + /// created. + /// + /// The stream context implementation may provide a mapping from the + /// file paths within the archive to the external file paths. + /// + public void Pack(IPackStreamContext streamContext, IEnumerable files) + { + if (files == null) + { + throw new ArgumentNullException("files"); + } + + this.Pack(streamContext, files, 0); + } + + /// + /// Creates an archive or chain of archives. + /// + /// A context interface to handle opening + /// and closing of archive and file streams. + /// The paths of the files in the archive (not + /// external file paths). + /// The maximum number of bytes for one + /// archive before the contents are chained to the next archive, or zero + /// for unlimited archive size. + /// The archive could not be + /// created. + /// + /// The stream context implementation may provide a mapping from the file + /// paths within the archive to the external file paths. + /// + public abstract void Pack( + IPackStreamContext streamContext, + IEnumerable files, + long maxArchiveSize); + + /// + /// Checks whether a Stream begins with a header that indicates + /// it is a valid archive. + /// + /// Stream for reading the archive file. + /// True if the stream is a valid archive + /// (with no offset); false otherwise. + public abstract bool IsArchive(Stream stream); + + /// + /// Gets the offset of an archive that is positioned 0 or more bytes + /// from the start of the Stream. + /// + /// A stream for reading the archive. + /// The offset in bytes of the archive, + /// or -1 if no archive is found in the Stream. + /// The archive must begin on a 4-byte boundary. + public virtual long FindArchiveOffset(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + long sectionSize = 4; + long length = stream.Length; + for (long offset = 0; offset <= length - sectionSize; offset += sectionSize) + { + stream.Seek(offset, SeekOrigin.Begin); + if (this.IsArchive(stream)) + { + return offset; + } + } + + return -1; + } + + /// + /// Gets information about all files in an archive stream. + /// + /// A stream for reading the archive. + /// Information about all files in the archive stream. + /// The stream is not a valid + /// archive. + public IList GetFileInfo(Stream stream) + { + return this.GetFileInfo(new BasicUnpackStreamContext(stream), null); + } + + /// + /// Gets information about files in an archive or archive chain. + /// + /// A context interface to handle opening + /// and closing of archive and file streams. + /// A predicate that can determine + /// which files to process, optional. + /// Information about files in the archive stream. + /// The archive provided + /// by the stream context is not valid. + /// + /// The predicate takes an internal file + /// path and returns true to include the file or false to exclude it. + /// + public abstract IList GetFileInfo( + IUnpackStreamContext streamContext, + Predicate fileFilter); + + /// + /// Gets the list of files in an archive Stream. + /// + /// A stream for reading the archive. + /// A list of the paths of all files contained in the + /// archive. + /// The stream is not a valid + /// archive. + public IList GetFiles(Stream stream) + { + return this.GetFiles(new BasicUnpackStreamContext(stream), null); + } + + /// + /// Gets the list of files in an archive or archive chain. + /// + /// A context interface to handle opening + /// and closing of archive and file streams. + /// A predicate that can determine + /// which files to process, optional. + /// An array containing the names of all files contained in + /// the archive or archive chain. + /// The archive provided + /// by the stream context is not valid. + /// + /// The predicate takes an internal file + /// path and returns true to include the file or false to exclude it. + /// + public IList GetFiles( + IUnpackStreamContext streamContext, + Predicate fileFilter) + { + if (streamContext == null) + { + throw new ArgumentNullException("streamContext"); + } + + IList files = + this.GetFileInfo(streamContext, fileFilter); + IList fileNames = new List(files.Count); + for (int i = 0; i < files.Count; i++) + { + fileNames.Add(files[i].Name); + } + + return fileNames; + } + + /// + /// Reads a single file from an archive stream. + /// + /// A stream for reading the archive. + /// The path of the file within the archive + /// (not the external file path). + /// A stream for reading the extracted file, or null + /// if the file does not exist in the archive. + /// The stream is not a valid + /// archive. + /// The entire extracted file is cached in memory, so this + /// method requires enough free memory to hold the file. + public Stream Unpack(Stream stream, string path) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + if (path == null) + { + throw new ArgumentNullException("path"); + } + + BasicUnpackStreamContext streamContext = + new BasicUnpackStreamContext(stream); + this.Unpack( + streamContext, + delegate(string match) + { + return String.Compare( + match, path, StringComparison.OrdinalIgnoreCase) == 0; + }); + + Stream extractStream = streamContext.FileStream; + if (extractStream != null) + { + extractStream.Position = 0; + } + + return extractStream; + } + + /// + /// Extracts files from an archive or archive chain. + /// + /// A context interface to handle opening + /// and closing of archive and file streams. + /// An optional predicate that can determine + /// which files to process. + /// The archive provided + /// by the stream context is not valid. + /// + /// The predicate takes an internal file + /// path and returns true to include the file or false to exclude it. + /// + public abstract void Unpack( + IUnpackStreamContext streamContext, + Predicate fileFilter); + + /// + /// Called by sublcasses to distribute a packing or unpacking progress + /// event to listeners. + /// + /// Event details. + protected void OnProgress(ArchiveProgressEventArgs e) + { + if (this.Progress != null) + { + this.Progress(this, e); + } + } + + /// + /// Disposes of resources allocated by the compression engine. + /// + /// If true, the method has been called + /// directly or indirectly by a user's code, so managed and unmanaged + /// resources will be disposed. If false, the method has been called by + /// the runtime from inside the finalizer, and only unmanaged resources + /// will be disposed. + protected virtual void Dispose(bool disposing) + { + } + + /// + /// Compresion utility function for converting old-style + /// date and time values to a DateTime structure. + /// + public static void DosDateAndTimeToDateTime( + short dosDate, short dosTime, out DateTime dateTime) + { + if (dosDate == 0 && dosTime == 0) + { + dateTime = DateTime.MinValue; + } + else + { + long fileTime; + SafeNativeMethods.DosDateTimeToFileTime(dosDate, dosTime, out fileTime); + dateTime = DateTime.FromFileTimeUtc(fileTime); + dateTime = new DateTime(dateTime.Ticks, DateTimeKind.Local); + } + } + + /// + /// Compresion utility function for converting a DateTime structure + /// to old-style date and time values. + /// + public static void DateTimeToDosDateAndTime( + DateTime dateTime, out short dosDate, out short dosTime) + { + dateTime = new DateTime(dateTime.Ticks, DateTimeKind.Utc); + long filetime = dateTime.ToFileTimeUtc(); + SafeNativeMethods.FileTimeToDosDateTime(ref filetime, out dosDate, out dosTime); + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/CompressionLevel.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/CompressionLevel.cs new file mode 100644 index 000000000..674091086 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/CompressionLevel.cs @@ -0,0 +1,34 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression +{ + /// + /// Specifies the compression level ranging from minimum compresion to + /// maximum compression, or no compression at all. + /// + /// + /// Although only four values are enumerated, any integral value between + /// and can also be used. + /// + public enum CompressionLevel + { + /// Do not compress files, only store. + None = 0, + + /// Minimum compression; fastest. + Min = 1, + + /// A compromize between speed and compression efficiency. + Normal = 6, + + /// Maximum compression; slowest. + Max = 10 + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/DuplicateStream.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/DuplicateStream.cs new file mode 100644 index 000000000..c77de1604 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/DuplicateStream.cs @@ -0,0 +1,233 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression +{ + using System; + using System.IO; + + /// + /// Duplicates a source stream by maintaining a separate position. + /// + /// + /// WARNING: duplicate streams are not thread-safe with respect to each other or the original stream. + /// If multiple threads use duplicate copies of the same stream, they must synchronize for any operations. + /// + public class DuplicateStream : Stream + { + private Stream source; + private long position; + + /// + /// Creates a new duplicate of a stream. + /// + /// source of the duplicate + public DuplicateStream(Stream source) + { + if (source == null) + { + throw new ArgumentNullException("source"); + } + + this.source = DuplicateStream.OriginalStream(source); + } + + /// + /// Gets the original stream that was used to create the duplicate. + /// + public Stream Source + { + get + { + return this.source; + } + } + + /// + /// Gets a value indicating whether the source stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead + { + get + { + return this.source.CanRead; + } + } + + /// + /// Gets a value indicating whether the source stream supports writing. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite + { + get + { + return this.source.CanWrite; + } + } + + /// + /// Gets a value indicating whether the source stream supports seeking. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek + { + get + { + return this.source.CanSeek; + } + } + + /// + /// Gets the length of the source stream. + /// + public override long Length + { + get + { + return this.source.Length; + } + } + + /// + /// Gets or sets the position of the current stream, + /// ignoring the position of the source stream. + /// + public override long Position + { + get + { + return this.position; + } + + set + { + this.position = value; + } + } + + /// + /// Retrieves the original stream from a possible duplicate stream. + /// + /// Possible duplicate stream. + /// If the stream is a DuplicateStream, returns + /// the duplicate's source; otherwise returns the same stream. + public static Stream OriginalStream(Stream stream) + { + DuplicateStream dupStream = stream as DuplicateStream; + return dupStream != null ? dupStream.Source : stream; + } + + /// + /// Flushes the source stream. + /// + public override void Flush() + { + this.source.Flush(); + } + + /// + /// Sets the length of the source stream. + /// + /// The desired length of the stream in bytes. + public override void SetLength(long value) + { + this.source.SetLength(value); + } + +#if !CORECLR + /// + /// Closes the underlying stream, effectively closing ALL duplicates. + /// + public override void Close() + { + this.source.Close(); + } +#endif + + /// + /// Disposes the stream + /// + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.source.Dispose(); + } + } + + /// + /// Reads from the source stream while maintaining a separate position + /// and not impacting the source stream's position. + /// + /// An array of bytes. When this method returns, the buffer + /// contains the specified byte array with the values between offset and + /// (offset + count - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in buffer at which to begin + /// storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// The total number of bytes read into the buffer. This can be less + /// than the number of bytes requested if that many bytes are not currently available, + /// or zero (0) if the end of the stream has been reached. + public override int Read(byte[] buffer, int offset, int count) + { + long saveSourcePosition = this.source.Position; + this.source.Position = this.position; + int read = this.source.Read(buffer, offset, count); + this.position = this.source.Position; + this.source.Position = saveSourcePosition; + return read; + } + + /// + /// Writes to the source stream while maintaining a separate position + /// and not impacting the source stream's position. + /// + /// An array of bytes. This method copies count + /// bytes from buffer to the current stream. + /// The zero-based byte offset in buffer at which + /// to begin copying bytes to the current stream. + /// The number of bytes to be written to the + /// current stream. + public override void Write(byte[] buffer, int offset, int count) + { + long saveSourcePosition = this.source.Position; + this.source.Position = this.position; + this.source.Write(buffer, offset, count); + this.position = this.source.Position; + this.source.Position = saveSourcePosition; + } + + /// + /// Changes the position of this stream without impacting the + /// source stream's position. + /// + /// A byte offset relative to the origin parameter. + /// A value of type SeekOrigin indicating the reference + /// point used to obtain the new position. + /// The new position within the current stream. + public override long Seek(long offset, SeekOrigin origin) + { + long originPosition = 0; + if (origin == SeekOrigin.Current) + { + originPosition = this.position; + } + else if (origin == SeekOrigin.End) + { + originPosition = this.Length; + } + + this.position = originPosition + offset; + return this.position; + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/IPackStreamContext.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/IPackStreamContext.cs new file mode 100644 index 000000000..a696af75c --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/IPackStreamContext.cs @@ -0,0 +1,124 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.IO; + + /// + /// This interface provides the methods necessary for the + /// to open and close streams for archives + /// and files. The implementor of this interface can use any kind of logic + /// to determine what kind of streams to open and where. + /// + public interface IPackStreamContext + { + /// + /// Gets the name of the archive with a specified number. + /// + /// The 0-based index of the archive + /// within the chain. + /// The name of the requested archive. May be an empty string + /// for non-chained archives, but may never be null. + /// The archive name is the name stored within the archive, used for + /// identification of the archive especially among archive chains. That + /// name is often, but not necessarily the same as the filename of the + /// archive package. + string GetArchiveName(int archiveNumber); + + /// + /// Opens a stream for writing an archive package. + /// + /// The 0-based index of the archive within + /// the chain. + /// The name of the archive that was returned + /// by . + /// True if the stream should be truncated when + /// opened (if it already exists); false if an existing stream is being + /// re-opened for writing additional data. + /// Instance of the compression engine + /// doing the operations. + /// A writable Stream where the compressed archive bytes will be + /// written, or null to cancel the archive creation. + /// + /// If this method returns null, the archive engine will throw a + /// FileNotFoundException. + /// + Stream OpenArchiveWriteStream( + int archiveNumber, + string archiveName, + bool truncate, + CompressionEngine compressionEngine); + + /// + /// Closes a stream where an archive package was written. + /// + /// The 0-based index of the archive within + /// the chain. + /// The name of the archive that was previously + /// returned by + /// . + /// A stream that was previously returned by + /// and is now ready to be closed. + /// + /// If there is another archive package in the chain, then after this stream + /// is closed a new stream will be opened. + /// + void CloseArchiveWriteStream(int archiveNumber, string archiveName, Stream stream); + + /// + /// Opens a stream to read a file that is to be included in an archive. + /// + /// The path of the file within the archive. This is often, + /// but not necessarily, the same as the relative path of the file outside + /// the archive. + /// Returned attributes of the opened file, to be + /// stored in the archive. + /// Returned last-modified time of the opened file, + /// to be stored in the archive. + /// A readable Stream where the file bytes will be read from before + /// they are compressed, or null to skip inclusion of the file and continue to + /// the next file. + [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")] + Stream OpenFileReadStream( + string path, + out FileAttributes attributes, + out DateTime lastWriteTime); + + /// + /// Closes a stream that has been used to read a file. + /// + /// The path of the file within the archive; the same as + /// the path provided + /// when the stream was opened. + /// A stream that was previously returned by + /// and is now ready to be closed. + void CloseFileReadStream(string path, Stream stream); + + /// + /// Gets extended parameter information specific to the compression + /// format being used. + /// + /// Name of the option being requested. + /// Parameters for the option; for per-file options, + /// the first parameter is typically the internal file path. + /// Option value, or null to use the default behavior. + /// + /// This method provides a way to set uncommon options during packaging, or a + /// way to handle aspects of compression formats not supported by the base library. + /// For example, this may be used by the zip compression library to + /// specify different compression methods/levels on a per-file basis. + /// The available option names, parameters, and expected return values + /// should be documented by each compression library. + /// + object GetOption(string optionName, object[] parameters); + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/IUnpackStreamContext.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/IUnpackStreamContext.cs new file mode 100644 index 000000000..c749d41cf --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/IUnpackStreamContext.cs @@ -0,0 +1,78 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression +{ + using System; + using System.IO; + + /// + /// This interface provides the methods necessary for the to open + /// and close streams for archives and files. The implementor of this interface can use any + /// kind of logic to determine what kind of streams to open and where + /// + public interface IUnpackStreamContext + { + /// + /// Opens the archive stream for reading. + /// + /// The zero-based index of the archive to open. + /// The name of the archive being opened. + /// Instance of the compression engine doing the operations. + /// A stream from which archive bytes are read, or null to cancel extraction + /// of the archive. + /// + /// When the first archive in a chain is opened, the name is not yet known, so the + /// provided value will be an empty string. When opening further archives, the + /// provided value is the next-archive name stored in the previous archive. This + /// name is often, but not necessarily, the same as the filename of the archive + /// package to be opened. + /// If this method returns null, the archive engine will throw a + /// FileNotFoundException. + /// + Stream OpenArchiveReadStream(int archiveNumber, string archiveName, CompressionEngine compressionEngine); + + /// + /// Closes a stream where an archive package was read. + /// + /// The archive number of the stream to close. + /// The name of the archive being closed. + /// The stream that was previously returned by + /// and is now ready to be closed. + void CloseArchiveReadStream(int archiveNumber, string archiveName, Stream stream); + + /// + /// Opens a stream for writing extracted file bytes. + /// + /// The path of the file within the archive. This is often, but + /// not necessarily, the same as the relative path of the file outside the archive. + /// The uncompressed size of the file to be extracted. + /// The last write time of the file to be extracted. + /// A stream where extracted file bytes are to be written, or null to skip + /// extraction of the file and continue to the next file. + /// + /// The implementor may use the path, size and date information to dynamically + /// decide whether or not the file should be extracted. + /// + Stream OpenFileWriteStream(string path, long fileSize, DateTime lastWriteTime); + + /// + /// Closes a stream where an extracted file was written. + /// + /// The path of the file within the archive. + /// The stream that was previously returned by + /// and is now ready to be closed. + /// The attributes of the extracted file. + /// The last write time of the file. + /// + /// The implementor may wish to apply the attributes and date to the newly-extracted file. + /// + void CloseFileWriteStream(string path, Stream stream, FileAttributes attributes, DateTime lastWriteTime); + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/OffsetStream.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/OffsetStream.cs new file mode 100644 index 000000000..7c346be76 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/OffsetStream.cs @@ -0,0 +1,228 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression +{ + using System; + using System.IO; + + /// + /// Wraps a source stream and offsets all read/write/seek calls by a given value. + /// + /// + /// This class is used to trick archive an packing or unpacking process + /// into reading or writing at an offset into a file, primarily for + /// self-extracting packages. + /// + public class OffsetStream : Stream + { + private Stream source; + private long sourceOffset; + + /// + /// Creates a new OffsetStream instance from a source stream + /// and using a specified offset. + /// + /// Underlying stream for which all calls will be offset. + /// Positive or negative number of bytes to offset. + public OffsetStream(Stream source, long offset) + { + if (source == null) + { + throw new ArgumentNullException("source"); + } + + this.source = source; + this.sourceOffset = offset; + + this.source.Seek(this.sourceOffset, SeekOrigin.Current); + } + + /// + /// Gets the underlying stream that this OffsetStream calls into. + /// + public Stream Source + { + get { return this.source; } + } + + /// + /// Gets the number of bytes to offset all calls before + /// redirecting to the underlying stream. + /// + public long Offset + { + get { return this.sourceOffset; } + } + + /// + /// Gets a value indicating whether the source stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead + { + get + { + return this.source.CanRead; + } + } + + /// + /// Gets a value indicating whether the source stream supports writing. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite + { + get + { + return this.source.CanWrite; + } + } + + /// + /// Gets a value indicating whether the source stream supports seeking. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek + { + get + { + return this.source.CanSeek; + } + } + + /// + /// Gets the effective length of the stream, which is equal to + /// the length of the source stream minus the offset. + /// + public override long Length + { + get { return this.source.Length - this.sourceOffset; } + } + + /// + /// Gets or sets the effective position of the stream, which + /// is equal to the position of the source stream minus the offset. + /// + public override long Position + { + get { return this.source.Position - this.sourceOffset; } + set { this.source.Position = value + this.sourceOffset; } + } + + /// + /// Reads a sequence of bytes from the source stream and advances + /// the position within the stream by the number of bytes read. + /// + /// An array of bytes. When this method returns, the buffer + /// contains the specified byte array with the values between offset and + /// (offset + count - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in buffer at which to begin + /// storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// The total number of bytes read into the buffer. This can be less + /// than the number of bytes requested if that many bytes are not currently available, + /// or zero (0) if the end of the stream has been reached. + public override int Read(byte[] buffer, int offset, int count) + { + return this.source.Read(buffer, offset, count); + } + + /// + /// Writes a sequence of bytes to the source stream and advances the + /// current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies count + /// bytes from buffer to the current stream. + /// The zero-based byte offset in buffer at which + /// to begin copying bytes to the current stream. + /// The number of bytes to be written to the + /// current stream. + public override void Write(byte[] buffer, int offset, int count) + { + this.source.Write(buffer, offset, count); + } + + /// + /// Reads a byte from the stream and advances the position within the + /// source stream by one byte, or returns -1 if at the end of the stream. + /// + /// The unsigned byte cast to an Int32, or -1 if at the + /// end of the stream. + public override int ReadByte() + { + return this.source.ReadByte(); + } + + /// + /// Writes a byte to the current position in the source stream and + /// advances the position within the stream by one byte. + /// + /// The byte to write to the stream. + public override void WriteByte(byte value) + { + this.source.WriteByte(value); + } + + /// + /// Flushes the source stream. + /// + public override void Flush() + { + this.source.Flush(); + } + + /// + /// Sets the position within the current stream, which is + /// equal to the position within the source stream minus the offset. + /// + /// A byte offset relative to the origin parameter. + /// A value of type SeekOrigin indicating + /// the reference point used to obtain the new position. + /// The new position within the current stream. + public override long Seek(long offset, SeekOrigin origin) + { + return this.source.Seek(offset + (origin == SeekOrigin.Begin ? this.sourceOffset : 0), origin) - this.sourceOffset; + } + + /// + /// Sets the effective length of the stream, which is equal to + /// the length of the source stream minus the offset. + /// + /// The desired length of the + /// current stream in bytes. + public override void SetLength(long value) + { + this.source.SetLength(value + this.sourceOffset); + } + +#if !CORECLR + /// + /// Closes the underlying stream. + /// + public override void Close() + { + this.source.Close(); + } +#endif + + /// + /// Disposes the underlying stream + /// + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.source.Dispose(); + } + } + + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/SafeNativeMethods.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/SafeNativeMethods.cs new file mode 100644 index 000000000..d9f17b69c --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/SafeNativeMethods.cs @@ -0,0 +1,38 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression +{ + using System.Runtime.InteropServices; + using System.Security; + +#if !CORECLR + [SuppressUnmanagedCodeSecurity] +#endif + internal static class SafeNativeMethods + { +#if !CORECLR + [DllImport("kernel32.dll", SetLastError = true)] +#else + [DllImport("api-ms-win-core-kernel32-legacy-l1-1-1.dll", SetLastError=true)] +#endif + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool DosDateTimeToFileTime( + short wFatDate, short wFatTime, out long fileTime); + +#if !CORECLR + [DllImport("kernel32.dll", SetLastError = true)] +#else + [DllImport("api-ms-win-core-kernel32-legacy-l1-1-1.dll", SetLastError=true)] +#endif + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool FileTimeToDosDateTime( + ref long fileTime, out short wFatDate, out short wFatTime); + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ConcatStream.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ConcatStream.cs new file mode 100644 index 000000000..fe8d219f7 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ConcatStream.cs @@ -0,0 +1,181 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Zip +{ + using System; + using System.IO; + + /// + /// Used to trick a DeflateStream into reading from or writing to + /// a series of (chunked) streams instead of a single steream. + /// + internal class ConcatStream : Stream + { + private Stream source; + private long position; + private long length; + private Action nextStreamHandler; + + public ConcatStream(Action nextStreamHandler) + { + if (nextStreamHandler == null) + { + throw new ArgumentNullException("nextStreamHandler"); + } + + this.nextStreamHandler = nextStreamHandler; + this.length = Int64.MaxValue; + } + + public Stream Source + { + get { return this.source; } + set { this.source = value; } + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanWrite + { + get { return true; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override long Length + { + get + { + return this.length; + } + } + + public override long Position + { + get { return this.position; } + set { throw new NotSupportedException(); } + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (this.source == null) + { + this.nextStreamHandler(this); + } + + count = (int) Math.Min(count, this.length - this.position); + + int bytesRemaining = count; + while (bytesRemaining > 0) + { + if (this.source == null) + { + throw new InvalidOperationException(); + } + + int partialCount = (int) Math.Min(bytesRemaining, + this.source.Length - this.source.Position); + + if (partialCount == 0) + { + this.nextStreamHandler(this); + continue; + } + + partialCount = this.source.Read( + buffer, offset + count - bytesRemaining, partialCount); + bytesRemaining -= partialCount; + this.position += partialCount; + } + + return count; + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (this.source == null) + { + this.nextStreamHandler(this); + } + + int bytesRemaining = count; + while (bytesRemaining > 0) + { + if (this.source == null) + { + throw new InvalidOperationException(); + } + + int partialCount = (int) Math.Min(bytesRemaining, + Math.Max(0, this.length - this.source.Position)); + + if (partialCount == 0) + { + this.nextStreamHandler(this); + continue; + } + + this.source.Write( + buffer, offset + count - bytesRemaining, partialCount); + bytesRemaining -= partialCount; + this.position += partialCount; + } + } + + public override void Flush() + { + if (this.source != null) + { + this.source.Flush(); + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + this.length = value; + } + +#if !CORECLR + /// + /// Closes underying stream + /// + public override void Close() + { + if (this.source != null) + { + this.source.Close(); + } + } +#endif + + /// + /// Disposes underlying stream + /// + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.source.Dispose(); + } + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/CrcStream.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/CrcStream.cs new file mode 100644 index 000000000..c2d1b9463 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/CrcStream.cs @@ -0,0 +1,271 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Zip +{ + using System.Diagnostics.CodeAnalysis; + using System.IO; + + /// + /// Wraps a source stream and calcaluates a CRC over all bytes that are read or written. + /// + /// + /// The CRC algorithm matches that used in the standard ZIP file format. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Crc")] + public class CrcStream : Stream + { + private Stream source; + private uint crc; + + /// + /// Creates a new CrcStream instance from a source stream. + /// + /// Underlying stream where bytes will be read from or written to. + public CrcStream(Stream source) + { + this.source = source; + } + + /// + /// Gets the current CRC over all bytes that have been read or written + /// since this instance was created. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Crc")] + public uint Crc + { + get + { + return this.crc; + } + } + + /// + /// Gets the underlying stream that this stream reads from or writes to. + /// + public Stream Source + { + get + { + return this.source; + } + } + + /// + /// Gets a value indicating whether the source stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead + { + get + { + return this.source.CanRead; + } + } + + /// + /// Gets a value indicating whether the source stream supports writing. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite + { + get + { + return this.source.CanWrite; + } + } + + /// + /// Gets a value indicating whether the source stream supports seeking. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek + { + get + { + return this.source.CanSeek; + } + } + + /// + /// Gets the length of the source stream. + /// + public override long Length + { + get + { + return this.source.Length; + } + } + + /// + /// Gets or sets the position of the source stream. + /// + public override long Position + { + get + { + return this.source.Position; + } + + set + { + this.source.Position = value; + } + } + + /// + /// Sets the position within the source stream. + /// + /// A byte offset relative to the origin parameter. + /// A value of type SeekOrigin indicating + /// the reference point used to obtain the new position. + /// The new position within the source stream. + /// + /// Note the CRC is only calculated over bytes that are actually read or + /// written, so any bytes skipped by seeking will not contribute to the CRC. + /// + public override long Seek(long offset, SeekOrigin origin) + { + return this.source.Seek(offset, origin); + } + + /// + /// Sets the length of the source stream. + /// + /// The desired length of the + /// stream in bytes. + public override void SetLength(long value) + { + this.source.SetLength(value); + } + + /// + /// Reads a sequence of bytes from the source stream and advances + /// the position within the stream by the number of bytes read. + /// + /// An array of bytes. When this method returns, the buffer + /// contains the specified byte array with the values between offset and + /// (offset + count - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in buffer at which to begin + /// storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// The total number of bytes read into the buffer. This can be less + /// than the number of bytes requested if that many bytes are not currently available, + /// or zero (0) if the end of the stream has been reached. + public override int Read(byte[] buffer, int offset, int count) + { + count = this.source.Read(buffer, offset, count); + this.UpdateCrc(buffer, offset, count); + return count; + } + + /// + /// Writes a sequence of bytes to the source stream and advances the + /// current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies count + /// bytes from buffer to the current stream. + /// The zero-based byte offset in buffer at which + /// to begin copying bytes to the current stream. + /// The number of bytes to be written to the + /// current stream. + public override void Write(byte[] buffer, int offset, int count) + { + this.source.Write(buffer, offset, count); + this.UpdateCrc(buffer, offset, count); + } + + /// + /// Flushes the source stream. + /// + public override void Flush() + { + this.source.Flush(); + } + +#if !CORECLR + /// + /// Closes the underlying stream. + /// + public override void Close() + { + this.source.Close(); + base.Close(); + } +#endif + + /// + /// Disposes underlying stream. + /// + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.source.Dispose(); + base.Dispose(); + } + } + + /// + /// Updates the CRC with a range of bytes that were read or written. + /// + private void UpdateCrc(byte[] buffer, int offset, int count) + { + this.crc = ~this.crc; + for( ; count > 0; count--, offset++) + { + this.crc = (this.crc >> 8) ^ + CrcStream.crcTable[(this.crc & 0xFF) ^ buffer[offset]]; + } + this.crc = ~this.crc; + } + + private static uint[] crcTable = MakeCrcTable(); + + /// + /// Computes a table that speeds up calculation of the CRC. + /// + private static uint[] MakeCrcTable() + { + const uint poly = 0x04C11DB7u; + uint[] crcTable = new uint[256]; + for(uint n = 0; n < 256; n++) + { + uint c = CrcStream.Reflect(n, 8); + c = c << 24; + for(uint k = 0; k < 8; k++) + { + c = (c << 1) ^ ((c & 0x80000000u) != 0 ? poly : 0); + } + crcTable[n] = CrcStream.Reflect(c, 32); + } + return crcTable; + } + + /// + /// Reflects the ordering of certain number of bits. For exmample when reflecting + /// one byte, bit one is swapped with bit eight, bit two with bit seven, etc. + /// + private static uint Reflect(uint value, int bits) + { + for (int i = 0; i < bits / 2; i++) + { + uint leftBit = 1u << (bits - 1 - i); + uint rightBit = 1u << i; + if (((value & leftBit) != 0) != ((value & rightBit) != 0)) + { + value ^= leftBit | rightBit; + } + } + return value; + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipCompressionMethod.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipCompressionMethod.cs new file mode 100644 index 000000000..0324f4618 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipCompressionMethod.cs @@ -0,0 +1,87 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Zip +{ + using System.Diagnostics.CodeAnalysis; + + /// + /// Identifies the compression method or "algorithm" + /// used for a single file within a zip archive. + /// + /// + /// Proprietary zip implementations may define additional compression + /// methods outside of those included here. + /// + public enum ZipCompressionMethod + { + /// + /// The file is stored (no compression) + /// + Store = 0, + + /// + /// The file is Shrunk + /// + Shrink = 1, + + /// + /// The file is Reduced with compression factor 1 + /// + Reduce1 = 2, + + /// + /// The file is Reduced with compression factor 2 + /// + Reduce2 = 3, + + /// + /// The file is Reduced with compression factor 3 + /// + Reduce3 = 4, + + /// + /// The file is Reduced with compression factor 4 + /// + Reduce4 = 5, + + /// + /// The file is Imploded + /// + Implode = 6, + + /// + /// The file is Deflated; + /// the most common and widely-compatible form of zip compression. + /// + Deflate = 8, + + /// + /// The file is Deflated using the enhanced Deflate64 method. + /// + Deflate64 = 9, + + /// + /// The file is compressed using the BZIP2 algorithm. + /// + BZip2 = 12, + + /// + /// The file is compressed using the LZMA algorithm. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Lzma")] + Lzma = 14, + + /// + /// The file is compressed using the PPMd algorithm. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ppmd")] + Ppmd = 98 + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipEngine.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipEngine.cs new file mode 100644 index 000000000..4babdb80a --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipEngine.cs @@ -0,0 +1,493 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Zip +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.IO.Compression; + + /// + /// Engine capable of packing and unpacking archives in the zip format. + /// + public partial class ZipEngine : CompressionEngine + { + private static Dictionary> + compressionStreamCreators; + private static Dictionary> + decompressionStreamCreators; + + private static void InitCompressionStreamCreators() + { + if (ZipEngine.compressionStreamCreators == null) + { + ZipEngine.compressionStreamCreators = new + Dictionary>(); + ZipEngine.decompressionStreamCreators = new + Dictionary>(); + + ZipEngine.RegisterCompressionStreamCreator( + ZipCompressionMethod.Store, + CompressionMode.Compress, + delegate(Stream stream) + { + return stream; + }); + ZipEngine.RegisterCompressionStreamCreator( + ZipCompressionMethod.Deflate, + CompressionMode.Compress, + delegate(Stream stream) + { + return new DeflateStream(stream, CompressionMode.Compress, true); + }); + ZipEngine.RegisterCompressionStreamCreator( + ZipCompressionMethod.Store, + CompressionMode.Decompress, + delegate(Stream stream) + { + return stream; + }); + ZipEngine.RegisterCompressionStreamCreator( + ZipCompressionMethod.Deflate, + CompressionMode.Decompress, + delegate(Stream stream) + { + return new DeflateStream(stream, CompressionMode.Decompress, true); + }); + } + } + + /// + /// Registers a delegate that can create a warpper stream for + /// compressing or uncompressing the data of a source stream. + /// + /// Compression method being registered. + /// Indicates registration for ether + /// compress or decompress mode. + /// Delegate being registered. + /// + /// For compression, the delegate accepts a stream that writes to the archive + /// and returns a wrapper stream that compresses bytes as they are written. + /// For decompression, the delegate accepts a stream that reads from the archive + /// and returns a wrapper stream that decompresses bytes as they are read. + /// This wrapper stream model follows the design used by + /// System.IO.Compression.DeflateStream, and indeed that class is used + /// to implement the Deflate compression method by default. + /// To unregister a delegate, call this method again and pass + /// null for the delegate parameter. + /// + /// + /// When the ZipEngine class is initialized, the Deflate compression method + /// is automatically registered like this: + /// + /// ZipEngine.RegisterCompressionStreamCreator( + /// ZipCompressionMethod.Deflate, + /// CompressionMode.Compress, + /// delegate(Stream stream) { + /// return new DeflateStream(stream, CompressionMode.Compress, true); + /// }); + /// ZipEngine.RegisterCompressionStreamCreator( + /// ZipCompressionMethod.Deflate, + /// CompressionMode.Decompress, + /// delegate(Stream stream) { + /// return new DeflateStream(stream, CompressionMode.Decompress, true); + /// }); + /// + public static void RegisterCompressionStreamCreator( + ZipCompressionMethod compressionMethod, + CompressionMode compressionMode, + Func creator) + { + ZipEngine.InitCompressionStreamCreators(); + if (compressionMode == CompressionMode.Compress) + { + ZipEngine.compressionStreamCreators[compressionMethod] = creator; + } + else + { + ZipEngine.decompressionStreamCreators[compressionMethod] = creator; + } + } + + // Progress data + private string currentFileName; + private int currentFileNumber; + private int totalFiles; + private long currentFileBytesProcessed; + private long currentFileTotalBytes; + private string mainArchiveName; + private string currentArchiveName; + private short currentArchiveNumber; + private short totalArchives; + private long currentArchiveBytesProcessed; + private long currentArchiveTotalBytes; + private long fileBytesProcessed; + private long totalFileBytes; + private string comment; + + /// + /// Creates a new instance of the zip engine. + /// + public ZipEngine() + : base() + { + ZipEngine.InitCompressionStreamCreators(); + } + + /// + /// Gets the comment from the last-examined archive, + /// or sets the comment to be added to any created archives. + /// + public string ArchiveComment + { + get + { + return this.comment; + } + set + { + this.comment = value; + } + } + + /// + /// Checks whether a Stream begins with a header that indicates + /// it is a valid archive file. + /// + /// Stream for reading the archive file. + /// True if the stream is a valid zip archive + /// (with no offset); false otherwise. + public override bool IsArchive(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + if (stream.Length - stream.Position < 4) + { + return false; + } + + BinaryReader reader = new BinaryReader(stream); + uint sig = reader.ReadUInt32(); + switch (sig) + { + case ZipFileHeader.LFHSIG: + case ZipEndOfCentralDirectory.EOCDSIG: + case ZipEndOfCentralDirectory.EOCD64SIG: + case ZipFileHeader.SPANSIG: + case ZipFileHeader.SPANSIG2: + return true; + default: + return false; + } + } + + /// + /// Gets the offset of an archive that is positioned 0 or more bytes + /// from the start of the Stream. + /// + /// A stream for reading the archive. + /// The offset in bytes of the archive, + /// or -1 if no archive is found in the Stream. + /// The archive must begin on a 4-byte boundary. + public override long FindArchiveOffset(Stream stream) + { + if (stream == null) + { + throw new ArgumentNullException("stream"); + } + + long offset = base.FindArchiveOffset(stream); + if (offset > 0) + { + // Some self-extract packages include the exe stub in file offset calculations. + // Check the first header directory offset to decide whether the entire + // archive needs to be offset or not. + + ZipEndOfCentralDirectory eocd = this.GetEOCD(null, stream); + if (eocd != null && eocd.totalEntries > 0) + { + stream.Seek(eocd.dirOffset, SeekOrigin.Begin); + + ZipFileHeader header = new ZipFileHeader(); + if (header.Read(stream, true) && header.localHeaderOffset < stream.Length) + { + stream.Seek(header.localHeaderOffset, SeekOrigin.Begin); + if (header.Read(stream, false)) + { + return 0; + } + } + } + } + + return offset; + } + + /// + /// Gets information about files in a zip archive or archive chain. + /// + /// A context interface to handle opening + /// and closing of archive and file streams. + /// A predicate that can determine + /// which files to process, optional. + /// Information about files in the archive stream. + /// The archive provided + /// by the stream context is not valid. + /// + /// The predicate takes an internal file + /// path and returns true to include the file or false to exclude it. + /// + public override IList GetFileInfo( + IUnpackStreamContext streamContext, + Predicate fileFilter) + { + if (streamContext == null) + { + throw new ArgumentNullException("streamContext"); + } + + lock (this) + { + IList headers = this.GetCentralDirectory(streamContext); + if (headers == null) + { + throw new ZipException("Zip central directory not found."); + } + + List files = new List(headers.Count); + foreach (ZipFileHeader header in headers) + { + if (!header.IsDirectory && + (fileFilter == null || fileFilter(header.fileName))) + { + files.Add(header.ToZipFileInfo()); + } + } + + return files.AsReadOnly(); + } + } + + /// + /// Reads all the file headers from the central directory in the main archive. + /// + private IList GetCentralDirectory(IUnpackStreamContext streamContext) + { + Stream archiveStream = null; + this.currentArchiveNumber = 0; + try + { + List headers = new List(); + archiveStream = this.OpenArchive(streamContext, 0); + + ZipEndOfCentralDirectory eocd = this.GetEOCD(streamContext, archiveStream); + if (eocd == null) + { + return null; + } + else if (eocd.totalEntries == 0) + { + return headers; + } + + headers.Capacity = (int)eocd.totalEntries; + + if (eocd.dirOffset > archiveStream.Length - ZipFileHeader.CFH_FIXEDSIZE) + { + streamContext.CloseArchiveReadStream( + this.currentArchiveNumber, String.Empty, archiveStream); + archiveStream = null; + } + else + { + archiveStream.Seek(eocd.dirOffset, SeekOrigin.Begin); + uint sig = new BinaryReader(archiveStream).ReadUInt32(); + if (sig != ZipFileHeader.CFHSIG) + { + streamContext.CloseArchiveReadStream( + this.currentArchiveNumber, String.Empty, archiveStream); + archiveStream = null; + } + } + + if (archiveStream == null) + { + this.currentArchiveNumber = (short)(eocd.dirStartDiskNumber + 1); + archiveStream = streamContext.OpenArchiveReadStream( + this.currentArchiveNumber, String.Empty, this); + + if (archiveStream == null) + { + return null; + } + } + + archiveStream.Seek(eocd.dirOffset, SeekOrigin.Begin); + + while (headers.Count < eocd.totalEntries) + { + ZipFileHeader header = new ZipFileHeader(); + if (!header.Read(archiveStream, true)) + { + throw new ZipException( + "Missing or invalid central directory file header"); + } + + headers.Add(header); + + if (headers.Count < eocd.totalEntries && + archiveStream.Position == archiveStream.Length) + { + streamContext.CloseArchiveReadStream( + this.currentArchiveNumber, String.Empty, archiveStream); + this.currentArchiveNumber++; + archiveStream = streamContext.OpenArchiveReadStream( + this.currentArchiveNumber, String.Empty, this); + if (archiveStream == null) + { + this.currentArchiveNumber = 0; + archiveStream = streamContext.OpenArchiveReadStream( + this.currentArchiveNumber, String.Empty, this); + } + } + } + + return headers; + } + finally + { + if (archiveStream != null) + { + streamContext.CloseArchiveReadStream( + this.currentArchiveNumber, String.Empty, archiveStream); + } + } + } + + /// + /// Locates and reads the end of central directory record near the + /// end of the archive. + /// + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "streamContext")] + private ZipEndOfCentralDirectory GetEOCD( + IUnpackStreamContext streamContext, Stream archiveStream) + { + BinaryReader reader = new BinaryReader(archiveStream); + long offset = archiveStream.Length + - ZipEndOfCentralDirectory.EOCD_RECORD_FIXEDSIZE; + while (offset >= 0) + { + archiveStream.Seek(offset, SeekOrigin.Begin); + + uint sig = reader.ReadUInt32(); + if (sig == ZipEndOfCentralDirectory.EOCDSIG) + { + break; + } + + offset--; + } + + if (offset < 0) + { + return null; + } + + ZipEndOfCentralDirectory eocd = new ZipEndOfCentralDirectory(); + archiveStream.Seek(offset, SeekOrigin.Begin); + if (!eocd.Read(archiveStream)) + { + throw new ZipException("Invalid end of central directory record"); + } + + if (eocd.dirOffset == (long)UInt32.MaxValue) + { + string saveComment = eocd.comment; + + archiveStream.Seek( + offset - Zip64EndOfCentralDirectoryLocator.EOCDL64_SIZE, + SeekOrigin.Begin); + + Zip64EndOfCentralDirectoryLocator eocdl = + new Zip64EndOfCentralDirectoryLocator(); + if (!eocdl.Read(archiveStream)) + { + throw new ZipException("Missing or invalid end of " + + "central directory record locator"); + } + + if (eocdl.dirStartDiskNumber == eocdl.totalDisks - 1) + { + // ZIP64 eocd is entirely in current stream. + archiveStream.Seek(eocdl.dirOffset, SeekOrigin.Begin); + if (!eocd.Read(archiveStream)) + { + throw new ZipException("Missing or invalid ZIP64 end of " + + "central directory record"); + } + } + else if (streamContext == null) + { + return null; + } + else + { + // TODO: handle EOCD64 spanning archives! + throw new NotImplementedException("Zip implementation does not " + + "handle end of central directory record that spans archives."); + } + + eocd.comment = saveComment; + } + + return eocd; + } + + private void ResetProgressData() + { + this.currentFileName = null; + this.currentFileNumber = 0; + this.totalFiles = 0; + this.currentFileBytesProcessed = 0; + this.currentFileTotalBytes = 0; + this.currentArchiveName = null; + this.currentArchiveNumber = 0; + this.totalArchives = 0; + this.currentArchiveBytesProcessed = 0; + this.currentArchiveTotalBytes = 0; + this.fileBytesProcessed = 0; + this.totalFileBytes = 0; + } + + private void OnProgress(ArchiveProgressType progressType) + { + ArchiveProgressEventArgs e = new ArchiveProgressEventArgs( + progressType, + this.currentFileName, + this.currentFileNumber >= 0 ? this.currentFileNumber : 0, + this.totalFiles, + this.currentFileBytesProcessed, + this.currentFileTotalBytes, + this.currentArchiveName, + this.currentArchiveNumber, + this.totalArchives, + this.currentArchiveBytesProcessed, + this.currentArchiveTotalBytes, + this.fileBytesProcessed, + this.totalFileBytes); + this.OnProgress(e); + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipException.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipException.cs new file mode 100644 index 000000000..dba4862c7 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipException.cs @@ -0,0 +1,44 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Zip +{ + using System; + + /// + /// Exception class for zip operations. + /// + public class ZipException : ArchiveException + { + /// + /// Creates a new ZipException with a specified error message and a reference to the + /// inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. If the + /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception + /// is raised in a catch block that handles the inner exception. + public ZipException(string message, Exception innerException) + : base(message, innerException) { } + + /// + /// Creates a new ZipException with a specified error message. + /// + /// The message that describes the error. + public ZipException(string message) + : this(message, null) { } + + /// + /// Creates a new ZipException. + /// + public ZipException() + : this(null, null) { } + + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipFileInfo.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipFileInfo.cs new file mode 100644 index 000000000..e15293f44 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipFileInfo.cs @@ -0,0 +1,87 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Zip +{ + using System; + using System.IO; + + /// + /// Object representing a compressed file within a zip package; provides operations for getting + /// the file properties and extracting the file. + /// + public class ZipFileInfo : ArchiveFileInfo + { + private long compressedLength; + private ZipCompressionMethod compressionMethod; + + /// + /// Creates a new ZipFileInfo object representing a file within a zip in a specified path. + /// + /// An object representing the zip archive containing the file. + /// The path to the file within the zip archive. Usually, this is a simple file + /// name, but if the zip archive contains a directory structure this may include the directory. + public ZipFileInfo(ZipInfo zipInfo, string filePath) + : base(zipInfo, filePath) + { + if (zipInfo == null) + { + throw new ArgumentNullException("zipInfo"); + } + } + + /// + /// Creates a new ZipFileInfo object with all parameters specified, + /// used internally when reading the metadata out of a zip archive. + /// + /// The internal path and name of the file in the zip archive. + /// The zip archive number where the file starts. + /// The stored attributes of the file. + /// The stored last write time of the file. + /// The uncompressed size of the file. + /// The compressed size of the file. + /// Compression algorithm used for this file. + internal ZipFileInfo( + string filePath, + int zipNumber, + FileAttributes attributes, + DateTime lastWriteTime, + long length, + long compressedLength, + ZipCompressionMethod compressionMethod) + : base(filePath, zipNumber, attributes, lastWriteTime, length) + { + this.compressedLength = compressedLength; + this.compressionMethod = compressionMethod; + } + + /// + /// Gets the compressed size of the file in bytes. + /// + public long CompressedLength + { + get + { + return this.compressedLength; + } + } + + /// + /// Gets the method used to compress this file. + /// + public ZipCompressionMethod CompressionMethod + { + get + { + return this.compressionMethod; + } + } + + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipFormat.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipFormat.cs new file mode 100644 index 000000000..2a9a48e47 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipFormat.cs @@ -0,0 +1,708 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Zip +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Text; + + [Flags] + internal enum ZipFileFlags : ushort + { + None = 0x0000, + Encrypt = 0x0001, + CompressOption1 = 0x0002, + CompressOption2 = 0x0004, + DataDescriptor = 0x0008, + StrongEncrypt = 0x0040, + UTF8 = 0x0800 + } + + internal enum ZipExtraFileFieldType : ushort + { + ZIP64 = 0x0001, + NTFS_TIMES = 0x000A, + NTFS_ACLS = 0x4453, + EXTIME = 0x5455 + } + + internal class ZipFileHeader + { + public const uint LFHSIG = 0x04034B50; + public const uint CFHSIG = 0x02014B50; + + public const uint SPANSIG = 0x08074b50; + public const uint SPANSIG2 = 0x30304b50; + + public const uint LFH_FIXEDSIZE = 30; + public const uint CFH_FIXEDSIZE = 46; + + public ushort versionMadeBy; + public ushort versionNeeded; + public ZipFileFlags flags; + public ZipCompressionMethod compressionMethod; + public short lastModTime; + public short lastModDate; + public uint crc32; + public uint compressedSize; + public uint uncompressedSize; + public ushort diskStart; + public ushort internalFileAttrs; + public uint externalFileAttrs; + public uint localHeaderOffset; + public string fileName; + public ZipExtraFileField[] extraFields; + public string fileComment; + public bool zip64; + + public ZipFileHeader() + { + this.versionMadeBy = 20; + this.versionNeeded = 20; + } + + public ZipFileHeader(ZipFileInfo fileInfo, bool zip64) + : this() + { + this.flags = ZipFileFlags.None; + this.compressionMethod = fileInfo.CompressionMethod; + this.fileName = Path.Combine(fileInfo.Path, fileInfo.Name); + CompressionEngine.DateTimeToDosDateAndTime( + fileInfo.LastWriteTime, out this.lastModDate, out this.lastModTime); + this.zip64 = zip64; + + if (this.zip64) + { + this.compressedSize = UInt32.MaxValue; + this.uncompressedSize = UInt32.MaxValue; + this.diskStart = UInt16.MaxValue; + this.versionMadeBy = 45; + this.versionNeeded = 45; + ZipExtraFileField field = new ZipExtraFileField(); + field.fieldType = ZipExtraFileFieldType.ZIP64; + field.SetZip64Data( + fileInfo.CompressedLength, + fileInfo.Length, + 0, + fileInfo.ArchiveNumber); + this.extraFields = new ZipExtraFileField[] { field }; + } + else + { + this.compressedSize = (uint) fileInfo.CompressedLength; + this.uncompressedSize = (uint) fileInfo.Length; + this.diskStart = (ushort) fileInfo.ArchiveNumber; + } + } + + [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "compressedSize")] + [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "uncompressedSize")] + [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "crc32")] + [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "localHeaderOffset")] + public void Update( + long compressedSize, + long uncompressedSize, + uint crc32, + long localHeaderOffset, + int archiveNumber) + { + this.crc32 = crc32; + + if (this.zip64) + { + this.compressedSize = UInt32.MaxValue; + this.uncompressedSize = UInt32.MaxValue; + this.localHeaderOffset = UInt32.MaxValue; + this.diskStart = UInt16.MaxValue; + + if (this.extraFields != null) + { + foreach (ZipExtraFileField field in this.extraFields) + { + if (field.fieldType == ZipExtraFileFieldType.ZIP64) + { + field.SetZip64Data( + compressedSize, + uncompressedSize, + localHeaderOffset, + archiveNumber); + } + } + } + } + else + { + this.compressedSize = (uint) compressedSize; + this.uncompressedSize = (uint) uncompressedSize; + this.localHeaderOffset = (uint) localHeaderOffset; + this.diskStart = (ushort) archiveNumber; + } + } + + public bool Read(Stream stream, bool central) + { + long startPos = stream.Position; + + if (stream.Length - startPos < + (central ? CFH_FIXEDSIZE : LFH_FIXEDSIZE)) + { + return false; + } + + BinaryReader reader = new BinaryReader(stream); + uint sig = reader.ReadUInt32(); + + if (sig == SPANSIG || sig == SPANSIG2) + { + // Spanned zip files may optionally begin with a special marker. + // Just ignore it and move on. + sig = reader.ReadUInt32(); + } + + if (sig != (central ? CFHSIG : LFHSIG)) + { + return false; + } + + this.versionMadeBy = (central ? reader.ReadUInt16() : (ushort) 0); + this.versionNeeded = reader.ReadUInt16(); + this.flags = (ZipFileFlags) reader.ReadUInt16(); + this.compressionMethod = (ZipCompressionMethod) reader.ReadUInt16(); + this.lastModTime = reader.ReadInt16(); + this.lastModDate = reader.ReadInt16(); + this.crc32 = reader.ReadUInt32(); + this.compressedSize = reader.ReadUInt32(); + this.uncompressedSize = reader.ReadUInt32(); + + this.zip64 = this.uncompressedSize == UInt32.MaxValue; + + int fileNameLength = reader.ReadUInt16(); + int extraFieldLength = reader.ReadUInt16(); + int fileCommentLength; + + if (central) + { + fileCommentLength = reader.ReadUInt16(); + + this.diskStart = reader.ReadUInt16(); + this.internalFileAttrs = reader.ReadUInt16(); + this.externalFileAttrs = reader.ReadUInt32(); + this.localHeaderOffset = reader.ReadUInt32(); + } + else + { + fileCommentLength = 0; + this.diskStart = 0; + this.internalFileAttrs = 0; + this.externalFileAttrs = 0; + this.localHeaderOffset = 0; + } + + if (stream.Length - stream.Position < + fileNameLength + extraFieldLength + fileCommentLength) + { + return false; + } + +#if CORECLR + Encoding headerEncoding = Encoding.UTF8; +#else + Encoding headerEncoding = ((this.flags | ZipFileFlags.UTF8) != 0 ? + Encoding.UTF8 : Encoding.GetEncoding(CultureInfo.CurrentCulture.TextInfo.OEMCodePage)); +#endif + + byte[] fileNameBytes = reader.ReadBytes(fileNameLength); + this.fileName = headerEncoding.GetString(fileNameBytes).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + + List fields = new List(); + while (extraFieldLength > 0) + { + ZipExtraFileField field = new ZipExtraFileField(); + if (!field.Read(stream, ref extraFieldLength)) + { + return false; + } + fields.Add(field); + if (field.fieldType == ZipExtraFileFieldType.ZIP64) + { + this.zip64 = true; + } + } + this.extraFields = fields.ToArray(); + + byte[] fileCommentBytes = reader.ReadBytes(fileCommentLength); + this.fileComment = headerEncoding.GetString(fileCommentBytes); + + return true; + } + + public void Write(Stream stream, bool central) + { + byte[] fileNameBytes = (this.fileName != null + ? Encoding.UTF8.GetBytes(this.fileName) : new byte[0]); + byte[] fileCommentBytes = (this.fileComment != null + ? Encoding.UTF8.GetBytes(this.fileComment) : new byte[0]); + bool useUtf8 = + (this.fileName != null && fileNameBytes.Length > this.fileName.Length) || + (this.fileComment != null && fileCommentBytes.Length > this.fileComment.Length); + if (useUtf8) + { + this.flags |= ZipFileFlags.UTF8; + } + + BinaryWriter writer = new BinaryWriter(stream); + writer.Write(central ? CFHSIG : LFHSIG); + if (central) + { + writer.Write(this.versionMadeBy); + } + writer.Write(this.versionNeeded); + writer.Write((ushort) this.flags); + writer.Write((ushort) this.compressionMethod); + writer.Write(this.lastModTime); + writer.Write(this.lastModDate); + writer.Write(this.crc32); + writer.Write(this.compressedSize); + writer.Write(this.uncompressedSize); + + ushort extraFieldLength = 0; + if (this.extraFields != null) + { + foreach (ZipExtraFileField field in this.extraFields) + { + if (field.data != null) + { + extraFieldLength += (ushort) (4 + field.data.Length); + } + } + } + + writer.Write((ushort) fileNameBytes.Length); + writer.Write(extraFieldLength); + + if (central) + { + writer.Write((ushort) fileCommentBytes.Length); + + writer.Write(this.diskStart); + writer.Write(this.internalFileAttrs); + writer.Write(this.externalFileAttrs); + writer.Write(this.localHeaderOffset); + } + + writer.Write(fileNameBytes); + + if (this.extraFields != null) + { + foreach (ZipExtraFileField field in this.extraFields) + { + if (field.data != null) + { + field.Write(stream); + } + } + } + + if (central) + { + writer.Write(fileCommentBytes); + } + } + + [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "compressedSize")] + [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "uncompressedSize")] + [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "crc32")] + [SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "localHeaderOffset")] + public void GetZip64Fields( + out long compressedSize, + out long uncompressedSize, + out long localHeaderOffset, + out int archiveNumber, + out uint crc) + { + compressedSize = this.compressedSize; + uncompressedSize = this.uncompressedSize; + localHeaderOffset = this.localHeaderOffset; + archiveNumber = this.diskStart; + crc = this.crc32; + + foreach (ZipExtraFileField field in this.extraFields) + { + if (field.fieldType == ZipExtraFileFieldType.ZIP64) + { + field.GetZip64Data( + out compressedSize, + out uncompressedSize, + out localHeaderOffset, + out archiveNumber); + } + } + } + + public ZipFileInfo ToZipFileInfo() + { + string name = this.fileName; + + long compressedSizeL; + long uncompressedSizeL; + long localHeaderOffsetL; + int archiveNumberL; + uint crc; + this.GetZip64Fields( + out compressedSizeL, + out uncompressedSizeL, + out localHeaderOffsetL, + out archiveNumberL, + out crc); + + DateTime dateTime; + CompressionEngine.DosDateAndTimeToDateTime( + this.lastModDate, + this.lastModTime, + out dateTime); + FileAttributes attrs = FileAttributes.Normal; + // TODO: look for attrs or times in extra fields + + return new ZipFileInfo(name, archiveNumberL, attrs, dateTime, + uncompressedSizeL, compressedSizeL, this.compressionMethod); + } + + public bool IsDirectory + { + get + { + return this.fileName != null && + (this.fileName.EndsWith("/", StringComparison.Ordinal) || + this.fileName.EndsWith("\\", StringComparison.Ordinal)); + } + } + + public int GetSize(bool central) + { + int size = 30; + + int fileNameSize = (this.fileName != null + ? Encoding.UTF8.GetByteCount(this.fileName) : 0); + size += fileNameSize; + + if (this.extraFields != null) + { + foreach (ZipExtraFileField field in this.extraFields) + { + if (field.data != null) + { + size += 4 + field.data.Length; + } + } + } + + if (central) + { + size += 16; + + int fileCommentSize = (this.fileComment != null + ? Encoding.UTF8.GetByteCount(this.fileComment) : 0); + size += fileCommentSize; + } + + return size; + } + } + + internal class ZipExtraFileField + { + public ZipExtraFileFieldType fieldType; + public byte[] data; + + public bool Read(Stream stream, ref int bytesRemaining) + { + if (bytesRemaining < 4) + { + return false; + } + + BinaryReader reader = new BinaryReader(stream); + + this.fieldType = (ZipExtraFileFieldType) reader.ReadUInt16(); + ushort dataSize = reader.ReadUInt16(); + bytesRemaining -= 4; + + if (bytesRemaining < dataSize) + { + return false; + } + + this.data = reader.ReadBytes(dataSize); + bytesRemaining -= dataSize; + + return true; + } + + public void Write(Stream stream) + { + BinaryWriter writer = new BinaryWriter(stream); + writer.Write((ushort) this.fieldType); + + byte[] dataBytes = (this.data != null ? this.data : new byte[0]); + writer.Write((ushort) dataBytes.Length); + writer.Write(dataBytes); + } + + public bool GetZip64Data( + out long compressedSize, + out long uncompressedSize, + out long localHeaderOffset, + out int diskStart) + { + uncompressedSize = 0; + compressedSize = 0; + localHeaderOffset = 0; + diskStart = 0; + + if (this.fieldType != ZipExtraFileFieldType.ZIP64 || + this.data == null || this.data.Length != 28) + { + return false; + } + + using (MemoryStream dataStream = new MemoryStream(this.data)) + { + BinaryReader reader = new BinaryReader(dataStream); + uncompressedSize = reader.ReadInt64(); + compressedSize = reader.ReadInt64(); + localHeaderOffset = reader.ReadInt64(); + diskStart = reader.ReadInt32(); + } + + return true; + } + + public bool SetZip64Data( + long compressedSize, + long uncompressedSize, + long localHeaderOffset, + int diskStart) + { + if (this.fieldType != ZipExtraFileFieldType.ZIP64) + { + return false; + } + + using (MemoryStream dataStream = new MemoryStream()) + { + BinaryWriter writer = new BinaryWriter(dataStream); + writer.Write(uncompressedSize); + writer.Write(compressedSize); + writer.Write(localHeaderOffset); + writer.Write(diskStart); + this.data = dataStream.ToArray(); + } + + return true; + } + } + + internal class ZipEndOfCentralDirectory + { + public const uint EOCDSIG = 0x06054B50; + public const uint EOCD64SIG = 0x06064B50; + + public const uint EOCD_RECORD_FIXEDSIZE = 22; + public const uint EOCD64_RECORD_FIXEDSIZE = 56; + + public ushort versionMadeBy; + public ushort versionNeeded; + public uint diskNumber; + public uint dirStartDiskNumber; + public long entriesOnDisk; + public long totalEntries; + public long dirSize; + public long dirOffset; + public string comment; + public bool zip64; + + public ZipEndOfCentralDirectory() + { + this.versionMadeBy = 20; + this.versionNeeded = 20; + } + + public bool Read(Stream stream) + { + long startPos = stream.Position; + + if (stream.Length - startPos < EOCD_RECORD_FIXEDSIZE) + { + return false; + } + + BinaryReader reader = new BinaryReader(stream); + uint sig = reader.ReadUInt32(); + + this.zip64 = false; + if (sig != EOCDSIG) + { + if (sig == EOCD64SIG) + { + this.zip64 = true; + } + else + { + return false; + } + } + + if (this.zip64) + { + if (stream.Length - startPos < EOCD64_RECORD_FIXEDSIZE) + { + return false; + } + + long recordSize = reader.ReadInt64(); + this.versionMadeBy = reader.ReadUInt16(); + this.versionNeeded = reader.ReadUInt16(); + this.diskNumber = reader.ReadUInt32(); + this.dirStartDiskNumber = reader.ReadUInt32(); + this.entriesOnDisk = reader.ReadInt64(); + this.totalEntries = reader.ReadInt64(); + this.dirSize = reader.ReadInt64(); + this.dirOffset = reader.ReadInt64(); + + // Ignore any extended zip64 eocd data. + long exDataSize = recordSize + 12 - EOCD64_RECORD_FIXEDSIZE; + + if (stream.Length - stream.Position < exDataSize) + { + return false; + } + + stream.Seek(exDataSize, SeekOrigin.Current); + + this.comment = null; + } + else + { + this.diskNumber = reader.ReadUInt16(); + this.dirStartDiskNumber = reader.ReadUInt16(); + this.entriesOnDisk = reader.ReadUInt16(); + this.totalEntries = reader.ReadUInt16(); + this.dirSize = reader.ReadUInt32(); + this.dirOffset = reader.ReadUInt32(); + + int commentLength = reader.ReadUInt16(); + + if (stream.Length - stream.Position < commentLength) + { + return false; + } + + byte[] commentBytes = reader.ReadBytes(commentLength); + this.comment = Encoding.UTF8.GetString(commentBytes); + } + + return true; + } + + public void Write(Stream stream) + { + BinaryWriter writer = new BinaryWriter(stream); + + if (this.zip64) + { + writer.Write(EOCD64SIG); + writer.Write((long) EOCD64_RECORD_FIXEDSIZE); + writer.Write(this.versionMadeBy); + writer.Write(this.versionNeeded); + writer.Write(this.diskNumber); + writer.Write(this.dirStartDiskNumber); + writer.Write(this.entriesOnDisk); + writer.Write(this.totalEntries); + writer.Write(this.dirSize); + writer.Write(this.dirOffset); + } + else + { + writer.Write(EOCDSIG); + writer.Write((ushort) Math.Min((int) UInt16.MaxValue, this.diskNumber)); + writer.Write((ushort) Math.Min((int) UInt16.MaxValue, this.dirStartDiskNumber)); + writer.Write((ushort) Math.Min((int) UInt16.MaxValue, this.entriesOnDisk)); + writer.Write((ushort) Math.Min((int) UInt16.MaxValue, this.totalEntries)); + writer.Write((uint) Math.Min((long) UInt32.MaxValue, this.dirSize)); + writer.Write((uint) Math.Min((long) UInt32.MaxValue, this.dirOffset)); + + byte[] commentBytes = (this.comment != null + ? Encoding.UTF8.GetBytes(this.comment) : new byte[0]); + writer.Write((ushort) commentBytes.Length); + writer.Write(commentBytes); + } + } + + public int GetSize(bool zip64Size) + { + if (zip64Size) + { + return 56; + } + else + { + int commentSize = (this.comment != null + ? Encoding.UTF8.GetByteCount(this.comment) : 0); + return 22 + commentSize; + } + } + } + + internal class Zip64EndOfCentralDirectoryLocator + { + public const uint EOCDL64SIG = 0x07064B50; + + public const uint EOCDL64_SIZE = 20; + + public uint dirStartDiskNumber; + public long dirOffset; + public uint totalDisks; + + public bool Read(Stream stream) + { + long startPos = stream.Position; + if (stream.Length - startPos < EOCDL64_SIZE) + { + return false; + } + + BinaryReader reader = new BinaryReader(stream); + uint sig = reader.ReadUInt32(); + + if (sig != EOCDL64SIG) + { + return false; + } + + this.dirStartDiskNumber = reader.ReadUInt32(); + this.dirOffset = reader.ReadInt64(); + this.totalDisks = reader.ReadUInt32(); + + return true; + } + + public void Write(Stream stream) + { + BinaryWriter writer = new BinaryWriter(stream); + writer.Write(EOCDL64SIG); + writer.Write(this.dirStartDiskNumber); + writer.Write(this.dirOffset); + writer.Write(this.totalDisks); + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipInfo.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipInfo.cs new file mode 100644 index 000000000..2aa36df01 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipInfo.cs @@ -0,0 +1,77 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Zip +{ + using System; + using System.Collections.Generic; + + /// + /// Object representing a zip file on disk; provides access to + /// file-based operations on the zip file. + /// + /// + /// Generally, the methods on this class are much easier to use than the + /// stream-based interfaces provided by the class. + /// + public class ZipInfo : ArchiveInfo + { + /// + /// Creates a new CabinetInfo object representing a zip file in a specified path. + /// + /// The path to the zip file. When creating a zip file, this file does not + /// necessarily exist yet. + public ZipInfo(string path) + : base(path) + { + } + + /// + /// Creates a compression engine that does the low-level work for + /// this object. + /// + /// A new instance. + /// + /// Each instance will be d + /// immediately after use. + /// + protected override CompressionEngine CreateCompressionEngine() + { + return new ZipEngine(); + } + + /// + /// Gets information about the files contained in the archive. + /// + /// A list of objects, each + /// containing information about a file in the archive. + public new IList GetFiles() + { + IList files = base.GetFiles(); + List zipFiles = new List(files.Count); + foreach (ZipFileInfo zipFile in files) zipFiles.Add(zipFile); + return zipFiles.AsReadOnly(); + } + + /// + /// Gets information about the certain files contained in the archive file. + /// + /// The search string, such as + /// "*.txt". + /// A list of objects, each containing + /// information about a file in the archive. + public new IList GetFiles(string searchPattern) + { + IList files = base.GetFiles(searchPattern); + List zipFiles = new List(files.Count); + foreach (ZipFileInfo zipFile in files) zipFiles.Add(zipFile); + return zipFiles.AsReadOnly(); + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipPacker.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipPacker.cs new file mode 100644 index 000000000..b7bfd2708 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipPacker.cs @@ -0,0 +1,501 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Zip +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.IO.Compression; + using CompressionLevel = Compression.CompressionLevel; + + public partial class ZipEngine + { + /// + /// Creates a zip archive or chain of zip archives. + /// + /// A context interface to handle opening + /// and closing of archive and file streams. + /// An array of file lists. Each list is + /// compressed into one stream in the archive. + /// The maximum number of bytes for one archive + /// before the contents are chained to the next archive, or zero for unlimited + /// archive size. + /// The archive could not be + /// created. + /// + /// The stream context implementation may provide a mapping from the file + /// paths within the archive to the external file paths. + /// + public override void Pack( + IPackStreamContext streamContext, + IEnumerable files, + long maxArchiveSize) + { + if (streamContext == null) + { + throw new ArgumentNullException("streamContext"); + } + + if (files == null) + { + throw new ArgumentNullException("files"); + } + + lock (this) + { + Stream archiveStream = null; + try + { + this.ResetProgressData(); + this.totalArchives = 1; + + object forceZip64Value = streamContext.GetOption("forceZip64", null); + bool forceZip64 = Convert.ToBoolean( + forceZip64Value, CultureInfo.InvariantCulture); + + // Count the total number of files and bytes to be compressed. + foreach (string file in files) + { + FileAttributes attributes; + DateTime lastWriteTime; + Stream fileStream = streamContext.OpenFileReadStream( + file, + out attributes, + out lastWriteTime); + if (fileStream != null) + { + this.totalFileBytes += fileStream.Length; + this.totalFiles++; + streamContext.CloseFileReadStream(file, fileStream); + } + } + + List fileHeaders = new List(); + this.currentFileNumber = -1; + + if (this.currentArchiveName == null) + { + this.mainArchiveName = streamContext.GetArchiveName(0); + this.currentArchiveName = this.mainArchiveName; + + if (string.IsNullOrWhiteSpace(this.currentArchiveName)) + { + throw new FileNotFoundException("No name provided for archive."); + } + } + + this.OnProgress(ArchiveProgressType.StartArchive); + + // Compress files one by one, saving header info for each. + foreach (string file in files) + { + ZipFileHeader fileHeader = this.PackOneFile( + streamContext, + file, + maxArchiveSize, + forceZip64, + ref archiveStream); + + if (fileHeader != null) + { + fileHeaders.Add(fileHeader); + } + + this.currentArchiveTotalBytes = (archiveStream != null ? + archiveStream.Position : 0); + this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes; + } + + bool zip64 = forceZip64 || this.totalFiles > UInt16.MaxValue; + + // Write the central directory composed of all the file headers. + uint centralDirStartArchiveNumber = 0; + long centralDirStartPosition = 0; + long centralDirSize = 0; + for (int i = 0; i < fileHeaders.Count; i++) + { + ZipFileHeader fileHeader = fileHeaders[i]; + + int headerSize = fileHeader.GetSize(true); + centralDirSize += headerSize; + + this.CheckArchiveWriteStream( + streamContext, + maxArchiveSize, + headerSize, + ref archiveStream); + + if (i == 0) + { + centralDirStartArchiveNumber = (uint)this.currentArchiveNumber; + centralDirStartPosition = archiveStream.Position; + } + + fileHeader.Write(archiveStream, true); + if (fileHeader.zip64) + { + zip64 = true; + } + } + + this.currentArchiveTotalBytes = + (archiveStream != null ? archiveStream.Position : 0); + this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes; + + ZipEndOfCentralDirectory eocd = new ZipEndOfCentralDirectory(); + eocd.dirStartDiskNumber = centralDirStartArchiveNumber; + eocd.entriesOnDisk = fileHeaders.Count; + eocd.totalEntries = fileHeaders.Count; + eocd.dirSize = centralDirSize; + eocd.dirOffset = centralDirStartPosition; + eocd.comment = this.comment; + + Zip64EndOfCentralDirectoryLocator eocdl = + new Zip64EndOfCentralDirectoryLocator(); + + int maxFooterSize = eocd.GetSize(false); + if (archiveStream != null && (zip64 || archiveStream.Position > + ((long)UInt32.MaxValue) - eocd.GetSize(false))) + { + maxFooterSize += eocd.GetSize(true) + (int) + Zip64EndOfCentralDirectoryLocator.EOCDL64_SIZE; + zip64 = true; + } + + this.CheckArchiveWriteStream( + streamContext, + maxArchiveSize, + maxFooterSize, + ref archiveStream); + eocd.diskNumber = (uint)this.currentArchiveNumber; + + if (zip64) + { + eocd.versionMadeBy = 45; + eocd.versionNeeded = 45; + eocd.zip64 = true; + eocdl.dirOffset = archiveStream.Position; + eocdl.dirStartDiskNumber = (uint)this.currentArchiveNumber; + eocdl.totalDisks = (uint)this.currentArchiveNumber + 1; + eocd.Write(archiveStream); + eocdl.Write(archiveStream); + + eocd.dirOffset = UInt32.MaxValue; + eocd.dirStartDiskNumber = UInt16.MaxValue; + } + + eocd.zip64 = false; + eocd.Write(archiveStream); + + this.currentArchiveTotalBytes = archiveStream.Position; + this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes; + } + finally + { + if (archiveStream != null) + { + streamContext.CloseArchiveWriteStream( + this.currentArchiveNumber, this.mainArchiveName, archiveStream); + this.OnProgress(ArchiveProgressType.FinishArchive); + } + } + } + } + + /// + /// Moves to the next archive in the sequence if necessary. + /// + private void CheckArchiveWriteStream( + IPackStreamContext streamContext, + long maxArchiveSize, + long requiredSize, + ref Stream archiveStream) + { + if (archiveStream != null && + archiveStream.Length > 0 && maxArchiveSize > 0) + { + long sizeRemaining = maxArchiveSize - archiveStream.Length; + if (sizeRemaining < requiredSize) + { + string nextArchiveName = streamContext.GetArchiveName( + this.currentArchiveNumber + 1); + + if (string.IsNullOrWhiteSpace(nextArchiveName)) + { + throw new FileNotFoundException("No name provided for archive #" + + this.currentArchiveNumber + 1); + } + + this.currentArchiveTotalBytes = archiveStream.Position; + this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes; + + streamContext.CloseArchiveWriteStream( + this.currentArchiveNumber, + nextArchiveName, + archiveStream); + archiveStream = null; + + this.OnProgress(ArchiveProgressType.FinishArchive); + + this.currentArchiveNumber++; + this.totalArchives++; + this.currentArchiveBytesProcessed = 0; + this.currentArchiveTotalBytes = 0; + } + } + + if (archiveStream == null) + { + if (this.currentArchiveNumber > 0) + { + this.OnProgress(ArchiveProgressType.StartArchive); + } + + archiveStream = streamContext.OpenArchiveWriteStream( + this.currentArchiveNumber, this.mainArchiveName, true, this); + + if (archiveStream == null) + { + throw new FileNotFoundException("Stream not provided for archive #" + + this.currentArchiveNumber); + } + } + } + + /// + /// Adds one file to a zip archive in the process of being created. + /// + private ZipFileHeader PackOneFile( + IPackStreamContext streamContext, + string file, + long maxArchiveSize, + bool forceZip64, + ref Stream archiveStream) + { + Stream fileStream = null; + int headerArchiveNumber = 0; + try + { + // TODO: call GetOption to get compression method for the specific file + ZipCompressionMethod compressionMethod = ZipCompressionMethod.Deflate; + if (this.CompressionLevel == CompressionLevel.None) + { + compressionMethod = ZipCompressionMethod.Store; + } + + Func compressionStreamCreator; + if (!ZipEngine.compressionStreamCreators.TryGetValue( + compressionMethod, out compressionStreamCreator)) + { + return null; + } + + FileAttributes attributes; + DateTime lastWriteTime; + fileStream = streamContext.OpenFileReadStream( + file, out attributes, out lastWriteTime); + if (fileStream == null) + { + return null; + } + + this.currentFileName = file; + this.currentFileNumber++; + + this.currentFileTotalBytes = fileStream.Length; + this.currentFileBytesProcessed = 0; + this.OnProgress(ArchiveProgressType.StartFile); + + ZipFileInfo fileInfo = new ZipFileInfo( + file, + this.currentArchiveNumber, + attributes, + lastWriteTime, + fileStream.Length, + 0, + compressionMethod); + + bool zip64 = forceZip64 || fileStream.Length >= (long)UInt32.MaxValue; + ZipFileHeader fileHeader = new ZipFileHeader(fileInfo, zip64); + + this.CheckArchiveWriteStream( + streamContext, + maxArchiveSize, + fileHeader.GetSize(false), + ref archiveStream); + + long headerPosition = archiveStream.Position; + fileHeader.Write(archiveStream, false); + headerArchiveNumber = this.currentArchiveNumber; + + uint crc; + long bytesWritten = this.PackFileBytes( + streamContext, + fileStream, + maxArchiveSize, + compressionStreamCreator, + ref archiveStream, + out crc); + + fileHeader.Update( + bytesWritten, + fileStream.Length, + crc, + headerPosition, + headerArchiveNumber); + + streamContext.CloseFileReadStream(file, fileStream); + fileStream = null; + + // Go back and rewrite the updated file header. + if (this.currentArchiveNumber == headerArchiveNumber) + { + long fileEndPosition = archiveStream.Position; + archiveStream.Seek(headerPosition, SeekOrigin.Begin); + fileHeader.Write(archiveStream, false); + archiveStream.Seek(fileEndPosition, SeekOrigin.Begin); + } + else + { + // The file spanned archives, so temporarily reopen + // the archive where it started. + string headerArchiveName = streamContext.GetArchiveName( + headerArchiveNumber + 1); + Stream headerStream = null; + try + { + headerStream = streamContext.OpenArchiveWriteStream( + headerArchiveNumber, headerArchiveName, false, this); + headerStream.Seek(headerPosition, SeekOrigin.Begin); + fileHeader.Write(headerStream, false); + } + finally + { + if (headerStream != null) + { + streamContext.CloseArchiveWriteStream( + headerArchiveNumber, headerArchiveName, headerStream); + } + } + } + + this.OnProgress(ArchiveProgressType.FinishFile); + + return fileHeader; + } + finally + { + if (fileStream != null) + { + streamContext.CloseFileReadStream( + this.currentFileName, fileStream); + } + } + } + + /// + /// Writes compressed bytes of one file to the archive, + /// keeping track of the CRC and number of bytes written. + /// + private long PackFileBytes( + IPackStreamContext streamContext, + Stream fileStream, + long maxArchiveSize, + Func compressionStreamCreator, + ref Stream archiveStream, + out uint crc) + { + long writeStartPosition = archiveStream.Position; + long bytesWritten = 0; + CrcStream fileCrcStream = new CrcStream(fileStream); + + ConcatStream concatStream = new ConcatStream( + delegate(ConcatStream s) + { + Stream sourceStream = s.Source; + bytesWritten += sourceStream.Position - writeStartPosition; + + this.CheckArchiveWriteStream( + streamContext, + maxArchiveSize, + 1, + ref sourceStream); + + writeStartPosition = sourceStream.Position; + s.Source = sourceStream; + }); + + concatStream.Source = archiveStream; + + if (maxArchiveSize > 0) + { + concatStream.SetLength(maxArchiveSize); + } + + Stream compressionStream = compressionStreamCreator(concatStream); + + try + { + byte[] buf = new byte[4096]; + long bytesRemaining = fileStream.Length; + int counter = 0; + while (bytesRemaining > 0) + { + int count = (int)Math.Min( + bytesRemaining, (long)buf.Length); + + count = fileCrcStream.Read(buf, 0, count); + if (count <= 0) + { + throw new ZipException( + "Failed to read file: " + this.currentFileName); + } + + compressionStream.Write(buf, 0, count); + bytesRemaining -= count; + + this.fileBytesProcessed += count; + this.currentFileBytesProcessed += count; + this.currentArchiveTotalBytes = concatStream.Source.Position; + this.currentArchiveBytesProcessed = this.currentArchiveTotalBytes; + + if (++counter % 16 == 0) // Report every 64K + { + this.OnProgress(ArchiveProgressType.PartialFile); + } + } + + if (compressionStream is DeflateStream) + { +#if CORECLR + compressionStream.Dispose(); +#else + compressionStream.Close(); +#endif + } + else + { + compressionStream.Flush(); + } + } + finally + { + archiveStream = concatStream.Source; + } + + bytesWritten += archiveStream.Position - writeStartPosition; + + crc = fileCrcStream.Crc; + + return bytesWritten; + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipUnpacker.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipUnpacker.cs new file mode 100644 index 000000000..90ee7597e --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Compression/Zip/ZipUnpacker.cs @@ -0,0 +1,342 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Archivers.Internal.Compression.Zip +{ + using System; + using System.Collections.Generic; + using System.IO; + + public partial class ZipEngine + { + /// + /// Extracts files from a zip archive or archive chain. + /// + /// A context interface to handle opening + /// and closing of archive and file streams. + /// An optional predicate that can determine + /// which files to process. + /// The archive provided + /// by the stream context is not valid. + /// + /// The predicate takes an internal file + /// path and returns true to include the file or false to exclude it. + /// + public override void Unpack( + IUnpackStreamContext streamContext, + Predicate fileFilter) + { + if (streamContext == null) + { + throw new ArgumentNullException("streamContext"); + } + + lock (this) + { + IList allHeaders = this.GetCentralDirectory(streamContext); + if (allHeaders == null) + { + throw new ZipException("Zip central directory not found."); + } + + IList headers = new List(allHeaders.Count); + foreach (ZipFileHeader header in allHeaders) + { + if (!header.IsDirectory && + (fileFilter == null || fileFilter(header.fileName))) + { + headers.Add(header); + } + } + + this.ResetProgressData(); + + // Count the total number of files and bytes to be compressed. + this.totalFiles = headers.Count; + foreach (ZipFileHeader header in headers) + { + long compressedSize; + long uncompressedSize; + long localHeaderOffset; + int archiveNumber; + uint crc; + header.GetZip64Fields( + out compressedSize, + out uncompressedSize, + out localHeaderOffset, + out archiveNumber, + out crc); + + this.totalFileBytes += uncompressedSize; + if (archiveNumber >= this.totalArchives) + { + this.totalArchives = (short)(archiveNumber + 1); + } + } + + this.currentArchiveNumber = -1; + this.currentFileNumber = -1; + Stream archiveStream = null; + try + { + foreach (ZipFileHeader header in headers) + { + this.currentFileNumber++; + this.UnpackOneFile(streamContext, header, ref archiveStream); + } + } + finally + { + if (archiveStream != null) + { + streamContext.CloseArchiveReadStream( + 0, String.Empty, archiveStream); + this.currentArchiveNumber--; + this.OnProgress(ArchiveProgressType.FinishArchive); + } + } + } + } + + /// + /// Unpacks a single file from an archive or archive chain. + /// + private void UnpackOneFile( + IUnpackStreamContext streamContext, + ZipFileHeader header, + ref Stream archiveStream) + { + ZipFileInfo fileInfo = null; + Stream fileStream = null; + try + { + Func compressionStreamCreator; + if (!ZipEngine.decompressionStreamCreators.TryGetValue( + header.compressionMethod, out compressionStreamCreator)) + { + // Silently skip files of an unsupported compression method. + return; + } + + long compressedSize; + long uncompressedSize; + long localHeaderOffset; + int archiveNumber; + uint crc; + header.GetZip64Fields( + out compressedSize, + out uncompressedSize, + out localHeaderOffset, + out archiveNumber, + out crc); + + if (this.currentArchiveNumber != archiveNumber + 1) + { + if (archiveStream != null) + { + streamContext.CloseArchiveReadStream( + this.currentArchiveNumber, + String.Empty, + archiveStream); + archiveStream = null; + + this.OnProgress(ArchiveProgressType.FinishArchive); + this.currentArchiveName = null; + } + + this.currentArchiveNumber = (short)(archiveNumber + 1); + this.currentArchiveBytesProcessed = 0; + this.currentArchiveTotalBytes = 0; + + archiveStream = this.OpenArchive( + streamContext, this.currentArchiveNumber); + + FileStream archiveFileStream = archiveStream as FileStream; + this.currentArchiveName = (archiveFileStream != null ? + Path.GetFileName(archiveFileStream.Name) : null); + + this.currentArchiveTotalBytes = archiveStream.Length; + this.currentArchiveNumber--; + this.OnProgress(ArchiveProgressType.StartArchive); + this.currentArchiveNumber++; + } + + archiveStream.Seek(localHeaderOffset, SeekOrigin.Begin); + + ZipFileHeader localHeader = new ZipFileHeader(); + if (!localHeader.Read(archiveStream, false) || + !ZipEngine.AreFilePathsEqual(localHeader.fileName, header.fileName)) + { + string msg = "Could not read file: " + header.fileName; + throw new ZipException(msg); + } + + fileInfo = header.ToZipFileInfo(); + + fileStream = streamContext.OpenFileWriteStream( + fileInfo.FullName, + fileInfo.Length, + fileInfo.LastWriteTime); + + if (fileStream != null) + { + this.currentFileName = header.fileName; + this.currentFileBytesProcessed = 0; + this.currentFileTotalBytes = fileInfo.Length; + this.currentArchiveNumber--; + this.OnProgress(ArchiveProgressType.StartFile); + this.currentArchiveNumber++; + + this.UnpackFileBytes( + streamContext, + fileInfo.FullName, + fileInfo.CompressedLength, + fileInfo.Length, + header.crc32, + fileStream, + compressionStreamCreator, + ref archiveStream); + } + } + finally + { + if (fileStream != null) + { + streamContext.CloseFileWriteStream( + fileInfo.FullName, + fileStream, + fileInfo.Attributes, + fileInfo.LastWriteTime); + + this.currentArchiveNumber--; + this.OnProgress(ArchiveProgressType.FinishFile); + this.currentArchiveNumber++; + } + } + } + + /// + /// Compares two internal file paths while ignoring case and slash differences. + /// + /// The first path to compare. + /// The second path to compare. + /// True if the paths are equivalent. + private static bool AreFilePathsEqual(string path1, string path2) + { + path1 = path1.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + path2 = path2.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + return String.Compare(path1, path2, StringComparison.OrdinalIgnoreCase) == 0; + } + + private Stream OpenArchive(IUnpackStreamContext streamContext, int archiveNumber) + { + Stream archiveStream = streamContext.OpenArchiveReadStream( + archiveNumber, String.Empty, this); + if (archiveStream == null && archiveNumber != 0) + { + archiveStream = streamContext.OpenArchiveReadStream( + 0, String.Empty, this); + } + + if (archiveStream == null) + { + throw new FileNotFoundException("Archive stream not provided."); + } + + return archiveStream; + } + + /// + /// Decompresses bytes for one file from an archive or archive chain, + /// checking the crc at the end. + /// + private void UnpackFileBytes( + IUnpackStreamContext streamContext, + string fileName, + long compressedSize, + long uncompressedSize, + uint crc, + Stream fileStream, + Func compressionStreamCreator, + ref Stream archiveStream) + { + CrcStream crcStream = new CrcStream(fileStream); + + ConcatStream concatStream = new ConcatStream( + delegate(ConcatStream s) + { + this.currentArchiveBytesProcessed = s.Source.Position; + streamContext.CloseArchiveReadStream( + this.currentArchiveNumber, + String.Empty, + s.Source); + + this.currentArchiveNumber--; + this.OnProgress(ArchiveProgressType.FinishArchive); + this.currentArchiveNumber += 2; + this.currentArchiveName = null; + this.currentArchiveBytesProcessed = 0; + this.currentArchiveTotalBytes = 0; + + s.Source = this.OpenArchive(streamContext, this.currentArchiveNumber); + + FileStream archiveFileStream = s.Source as FileStream; + this.currentArchiveName = (archiveFileStream != null ? + Path.GetFileName(archiveFileStream.Name) : null); + + this.currentArchiveTotalBytes = s.Source.Length; + this.currentArchiveNumber--; + this.OnProgress(ArchiveProgressType.StartArchive); + this.currentArchiveNumber++; + }); + + concatStream.Source = archiveStream; + concatStream.SetLength(compressedSize); + + Stream decompressionStream = compressionStreamCreator(concatStream); + + try + { + byte[] buf = new byte[4096]; + long bytesRemaining = uncompressedSize; + int counter = 0; + while (bytesRemaining > 0) + { + int count = (int)Math.Min(buf.Length, bytesRemaining); + count = decompressionStream.Read(buf, 0, count); + crcStream.Write(buf, 0, count); + bytesRemaining -= count; + + this.fileBytesProcessed += count; + this.currentFileBytesProcessed += count; + this.currentArchiveBytesProcessed = concatStream.Source.Position; + + if (++counter % 16 == 0) // Report every 64K + { + this.currentArchiveNumber--; + this.OnProgress(ArchiveProgressType.PartialFile); + this.currentArchiveNumber++; + } + } + } + finally + { + archiveStream = concatStream.Source; + } + + crcStream.Flush(); + + if (crcStream.Crc != crc) + { + throw new ZipException("CRC check failed for file: " + fileName); + } + } + } +} + diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/Properties/AssemblyInfo.cs b/src/Microsoft.PackageManagement.ArchiverProviders/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..5c22e4f39 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/Properties/AssemblyInfo.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. + +[assembly: AssemblyTitle("Microsoft.PackageManagement.Archivers")] +[assembly: AssemblyDescription("Archiver providers for Zip and Cab files")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("Microsoft.PackageManagement.Archivers")] +[assembly: AssemblyCopyright("Copyright Microsoft © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM + +[assembly: Guid("78cd1d7f-c276-4dbf-bb7a-a323dd6d96ed")] \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/ZipArchiver.cs b/src/Microsoft.PackageManagement.ArchiverProviders/ZipArchiver.cs new file mode 100644 index 000000000..91b869054 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/ZipArchiver.cs @@ -0,0 +1,137 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Archivers.Internal { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using Compression; + using Compression.Zip; + using PackageManagement.Internal; + using PackageManagement.Internal.Implementation; + + public class ZipArchiver { + private static readonly Dictionary _features = new Dictionary { + {Constants.Features.SupportedExtensions, new[] {"zip"}}, + {Constants.Features.MagicSignatures, new[] {Constants.Signatures.Zip}}, + }; + + public void InitializeProvider(Request request) { + } + + /// + /// Returns a collection of strings to the client advertizing features this provider supports. + /// + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void GetFeatures(Request request) { + // Nice-to-have put a debug message in that tells what's going on. + if( request == null ) { + throw new ArgumentNullException("request"); + } + + request.Debug("Calling '{0}::GetFeatures' ", ArchiverName ); + + foreach (var feature in _features) { + request.Yield(feature); + } + } + + /// + /// Returns the name of the Provider. + /// + /// + public string ArchiverName { + get { return "zipfile"; } + } + + /// + /// Returns the version of the Provider. + /// + /// The version of this provider + public string ProviderVersion { + get { + return "1.0.0.0"; + } + } + + + /// + /// This is just here as to give us some possibility of knowing when an unexception happens... + /// At the very least, we'll write it to the system debug channel, so a developer can find it if they are looking for it. + /// + public void OnUnhandledException(string methodName, Exception exception) { + if( exception != null ) { + Debug.WriteLine("Unexpected Exception thrown in '{0}::{1}' -- {2}\\{3}\r\n{4}", ArchiverName, methodName, exception.GetType().Name, exception.Message, exception.StackTrace); + } + } + + public IEnumerable UnpackArchive(string localFilename, string destinationFolder, Request request) { + return _UnpackArchive(localFilename, destinationFolder, request); + } + + private IEnumerable _UnpackArchive(string localFilename, string destinationFolder, Request request) { + var info = new ZipInfo(localFilename); + var files = info.GetFiles(); + var percent = 0; + var index = 0; + var processed = new List(); + + // request.Debug("Unpacking {0} {1}", localFilename, destinationFolder); + var pid = request.StartProgress(0, "Unpacking Archive '{0}' ", Path.GetFileName(localFilename)); + try { + info.Unpack(destinationFolder, (sender, args) => { + if (args.ProgressType == ArchiveProgressType.FinishFile) { + processed.Add(Path.Combine(destinationFolder, args.CurrentFileName)); + index++; + var complete = (index*100)/files.Count; + if (complete != percent) { + percent = complete; + request.Progress(pid, percent, "Unpacked {0}", args.CurrentFileName); + } + /* + * Does not currently support cancellation . + * Todo: add cancellation support to DTF compression classes. + * */ + if (request.IsCanceled) { + throw new OperationCanceledException("cancelling"); + } + } + }); + } catch (OperationCanceledException) { + // no worries. + } + + // request.Debug("DONE Unpacking {0} {1}", localFilename, destinationFolder); + request.CompleteProgress(pid, true); + + // return the list of files to the parent. + return processed.ToArray(); + } + + public bool IsSupportedFile(string localFilename) { + try { + var ze = new ZipEngine(); + using (var zipFile = File.OpenRead(localFilename)) { + return ze.IsArchive(zipFile); + } + } catch { + } + return false; + } + } +} diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/provider.manifest b/src/Microsoft.PackageManagement.ArchiverProviders/provider.manifest new file mode 100644 index 000000000..f28db3b84 --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/provider.manifest @@ -0,0 +1,8 @@ + + + + diff --git a/src/Microsoft.PackageManagement.ArchiverProviders/resources/Microsoft.PackageManagement.Archivers.Resources.Messages.resx b/src/Microsoft.PackageManagement.ArchiverProviders/resources/Microsoft.PackageManagement.Archivers.Resources.Messages.resx new file mode 100644 index 000000000..4fdb1b6af --- /dev/null +++ b/src/Microsoft.PackageManagement.ArchiverProviders/resources/Microsoft.PackageManagement.Archivers.Resources.Messages.resx @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.CoreProviders/Bootstrap/BootstrapProvider.cs b/src/Microsoft.PackageManagement.CoreProviders/Bootstrap/BootstrapProvider.cs new file mode 100644 index 000000000..aa849feb5 --- /dev/null +++ b/src/Microsoft.PackageManagement.CoreProviders/Bootstrap/BootstrapProvider.cs @@ -0,0 +1,712 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Providers.Internal.Bootstrap { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Management.Automation; + using System.Threading.Tasks; + using PackageManagement.Internal; + using PackageManagement.Internal.Api; + using PackageManagement.Internal.Implementation; + using PackageManagement.Internal.Packaging; + using PackageManagement.Packaging; + using PackageManagement.Internal.Utility.Extensions; + using PackageManagement.Internal.Utility.Plugin; + using PackageManagement.Internal.Utility.Versions; + using Directory = System.IO.Directory; + using ErrorCategory = PackageManagement.Internal.ErrorCategory; + using File = System.IO.File; + using System.IO.Compression; + + public class BootstrapProvider { + private static readonly Dictionary _features = new Dictionary { + // {Constants.Features.SupportedSchemes, new[] {"http", "https", "file"}}, + // {Constants.Features.SupportedExtensions, new[] {"exe", "msi"}}, + {Constants.Features.MagicSignatures, Constants.Empty}, + {Constants.Features.AutomationOnly, Constants.Empty} + }; + + private const WildcardOptions WildcardOptions = System.Management.Automation.WildcardOptions.CultureInvariant | System.Management.Automation.WildcardOptions.IgnoreCase; + + private static IEqualityComparer PackageEqualityComparer = new PackageManagement.Internal.Utility.Extensions.EqualityComparer( + (x, y) => x.Name.EqualsIgnoreCase(y.Name) && x.Version.EqualsIgnoreCase(y.Version), (x) => (x.Name + x.Version).GetHashCode()); + + private PackageManagementService PackageManagementService { + get { + return PackageManager.Instance as PackageManagementService; + } + } + + /// + /// Returns the name of the Provider. + /// + /// + /// the name of the package provider + public string PackageProviderName { + get { + return "Bootstrap"; + } + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Plugin requirement.")] + public void InitializeProvider(BootstrapRequest request) { + // we should go find out what's available once here just to make sure that + // we have a list + try { + request.Debug("Initialize Bootstrapper"); + Task.Factory.StartNew(() => { + // we can do this asynchronously, it'll cut down on any startup delay when the network is slow or unavailable. + try { + PackageManagementService.BootstrappableProviderNames = request.Providers.Select(provider => provider.Name).ToArray(); + + } catch (Exception e) { + // if we have a serious problem, it just means we can't bootstrap those providers anyway. + // in the event of a catastrophic failure, request isn't going to be valid anymore (and hence the user won't see it) + // but we can send the error to the system debug output. + e.Dump(); + } + }); + } catch (Exception e) { + e.Dump(); + } + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Plugin requirement.")] + public void GetFeatures(BootstrapRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + request.Debug("Calling 'Bootstrap::GetFeatures'"); + foreach (var feature in _features) { + request.Yield(feature); + } + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Plugin requirement.")] + public void GetDynamicOptions(string category, BootstrapRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + request.Debug("Calling 'Bootstrap::GetDynamicOptions ({0})'", category); + + switch ((category ?? string.Empty).ToLowerInvariant()) { + case "package": + break; + + case "source": + break; + + case "install": + request.YieldDynamicOption("DestinationPath", "Folder", false); + request.YieldDynamicOption("Scope", "String", false, new[] { "CurrentUser", "AllUsers" }); + request.YieldDynamicOption("DisplayLongSourceName", "Switch", false); + break; + } + } + + public void ResolvePackageSources(BootstrapRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + request.Debug("Calling ResolvePackageSources"); + + try { + if (request.LocalSource.Any()) { + foreach (var source in request.LocalSource) { + request.YieldPackageSource(source, source, false, true, true); + } + return; + } + + foreach (var source in request._urls) { + + request.YieldPackageSource(source.AbsoluteUri, source.AbsoluteUri, false, true, true); + } + + } catch (Exception e) { + e.Dump(); + } + } + + public void FindPackage(string name, string requiredVersion, string minimumVersion, string maximumVersion, int id, BootstrapRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + request.Verbose(Resources.Messages.FindingPackage, string.Format(CultureInfo.CurrentCulture, "{0}::FindPackage' '{1}','{2}','{3}','{4}'", PackageProviderName, name, requiredVersion, minimumVersion, maximumVersion)); + + if (name != null && name.EqualsIgnoreCase("PackageManagement")) { + // they are looking for PackageManagement itself. + // future todo: let PackageManagement update itself. + return; + } + + if (request.LocalSource.Any()) { + // find a provider from given path + request.FindProviderFromFile(name, requiredVersion, minimumVersion, maximumVersion); + return; + } + + // are they are looking for a specific provider? + if (string.IsNullOrWhiteSpace(name) || WildcardPattern.ContainsWildcardCharacters(name)) { + + + // no, return all providers that match the range. + var wildcardPattern = new WildcardPattern(name, WildcardOptions); + + if (request.GetOptionValue("AllVersions").IsTrue()) { + // Feed.Query() can return an empty provider, so here we need to execlude it by checking p.Name !=null or empty. + foreach (var p in request.Providers.Distinct(PackageEqualityComparer).Where(p => !string.IsNullOrWhiteSpace(p.Name) && (string.IsNullOrWhiteSpace(name) || wildcardPattern.IsMatch(p.Name)))) { + FindPackage(p.Name, null, "0.0", null, 0, request); + } + return; + } + + if (request.Providers.Distinct(PackageEqualityComparer).Where(p => string.IsNullOrWhiteSpace(name) || wildcardPattern.IsMatch(p.Name)).Any(p => !request.YieldFromSwidtag(p, requiredVersion, minimumVersion, maximumVersion, name))) { + // if there is a problem, exit. + return; + } + } else { + // return just the one they asked for. + + // asked for a specific version? + if (!string.IsNullOrWhiteSpace(requiredVersion)) { + request.YieldFromSwidtag(request.GetProvider(name, requiredVersion), name); + return; + } + + if (request.GetOptionValue("AllVersions").IsTrue()) { + if (request.GetProviderAll(name, minimumVersion, maximumVersion).Distinct(PackageEqualityComparer).Any(provider => !request.YieldFromSwidtag(provider, name))) { + // if there is a problem, exit. + return; + } + return; + } + + // asked for a version range? + if (!string.IsNullOrWhiteSpace(minimumVersion) || !string.IsNullOrEmpty(maximumVersion)) { + if (request.GetProvider(name, minimumVersion, maximumVersion).Distinct(PackageEqualityComparer).Any(provider => !request.YieldFromSwidtag(provider, name))) { + // if there is a problem, exit. + return; + } + return; + } + + // just return by name + request.YieldFromSwidtag(request.GetProvider(name), name); + } + + // return any matches in the name + } + + /// + /// Returns the packages that are installed + /// + /// the package name to match. Empty or null means match everything + /// + /// the specific version asked for. If this parameter is specified (ie, not null or empty + /// string) then the minimum and maximum values are ignored + /// + /// + /// the minimum version of packages to return . If the requiredVersion parameter + /// is specified (ie, not null or empty string) this should be ignored + /// + /// + /// the maximum version of packages to return . If the requiredVersion parameter + /// is specified (ie, not null or empty string) this should be ignored + /// + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Plugin requirement.")] + public void GetInstalledPackages(string name, string requiredVersion, string minimumVersion, string maximumVersion, BootstrapRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + request.Debug("Calling '{0}::GetInstalledPackages' '{1}','{2}','{3}','{4}'", PackageProviderName, name, requiredVersion, minimumVersion, maximumVersion); + + //search under the providerAssembies folder for the installed providers + var providers = PackageManagementService.AllProvidersFromProviderAssembliesLocation(request).Select(providerFileAssembly => { + + //get the provider's name\version + var versionFolder = Path.GetDirectoryName(providerFileAssembly); + + if (string.IsNullOrWhiteSpace(versionFolder)) { + return null; + } + + Version ver; + if (!Version.TryParse(Path.GetFileName(versionFolder), out ver)) { + //this will cover whether the providerFileAssembly is at top level as well as a bad version folder + //skip if the provider is at the top level as they are imported already via LoadProviders() during the initialization. + //the provider will be handled PackageManagementService.DynamicProviders below. + return null; + } + + var providerNameFolder = Path.GetDirectoryName(versionFolder); + if (!string.IsNullOrWhiteSpace(providerNameFolder)) { + var providerName = Path.GetFileName(providerNameFolder); + if (!string.IsNullOrWhiteSpace(providerName)) { + return new { + Name = providerName, + Version = (FourPartVersion)ver, + ProviderPath = providerFileAssembly + }; + } + } + + return null; + }).WhereNotNull(); + + // return all the dynamic package providers as packages + providers = providers.Concat(PackageManagementService.DynamicProviders.Select(each => new { + Name = each.ProviderName, + each.Version, + each.ProviderPath + })).Distinct(); + + var pp = request.LocalSource.Any() ? providers.Select(each => request.GetProviderFromFile(each.ProviderPath, false, true)).WhereNotNull() : + providers.Select(each => request.GetProvider(each.Name, each.Version)).WhereNotNull(); + + foreach (var p in pp) { + request.YieldFromSwidtag(p, requiredVersion, minimumVersion, maximumVersion, name); + } + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Plugin requirement.")] + public void DownloadPackage(string fastPath, string location, BootstrapRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + request.Debug("Calling 'Bootstrap::DownloadPackage'"); + } + + private bool InstallProviderFromInstaller(Package provider, Link link, string fastPath, BootstrapRequest request) { + switch (link.MediaType) { + case Iso19770_2.MediaType.MsiPackage: + case Iso19770_2.MediaType.MsuPackage: + return InstallPackageFile(provider, fastPath, request); + + case Iso19770_2.MediaType.PackageReference: + // let the core figure out how to install this package + var packages = PackageManagementService.FindPackageByCanonicalId(link.HRef.AbsoluteUri, request).ToArray(); + switch (packages.Length) { + case 0: + request.Warning("Unable to resolve package reference '{0}'", link.HRef); + return false; + + case 1: + return InstallPackageReference(provider, fastPath, request, packages); + + default: + request.Warning("Package Reference '{0}' resolves to {1} packages.", link.HRef, packages.Length); + return false; + } + + case Iso19770_2.MediaType.NuGetPackage: + return InstallNugetPackage(provider, link, fastPath, request); + + default: + request.Warning("Provider '{0}' with link '{1}' has unknown media type '{2}'.", provider.Name, link.HRef, link.MediaType); + return false; + } + } + + private bool InstallNugetPackage(Package provider, Link link, string fastPath, BootstrapRequest request) + { + // download the nuget package + string downloadedNupkg = request.DownloadAndValidateFile(provider._swidtag); + + if (downloadedNupkg != null) + { + // extracted folder + string extractedFolder = String.Concat(FilesystemExtensions.GenerateTemporaryFileOrDirectoryNameInTempDirectory()); + + try + { + //unzip the file + ZipFile.ExtractToDirectory(downloadedNupkg, extractedFolder); + + if (Directory.Exists(extractedFolder)) + { + string versionFolder = Path.Combine(request.DestinationPath(request), provider.Name, provider.Version); + // tool folder is where we find things like nuget.exe + string toolFolder = Path.Combine(extractedFolder, "tools"); + string libFolder = Path.Combine(extractedFolder, "lib"); + + // create the directory version folder if not exist + if (!Directory.Exists(versionFolder)) + { + Directory.CreateDirectory(versionFolder); + } + + // copy the tools directory + if (Directory.Exists(toolFolder)) + { + string destinationToolFolder = Path.Combine(versionFolder, "tools"); + + if (!Directory.Exists(destinationToolFolder)) + { + Directory.CreateDirectory(destinationToolFolder); + } + + foreach (string child in Directory.EnumerateFiles(toolFolder)) + { + try + { + // try copy and overwrite + File.Copy(child, Path.Combine(destinationToolFolder, Path.GetFileName(child)), true); + } + catch (Exception e) + { + request.Debug(e.StackTrace); + if (!(e is UnauthorizedAccessException || e is IOException)) + { + // something wrong, delete the version folder + versionFolder.TryHardToDelete(); + return false; + } + + // otherwise this means the file is just being used. so just moves on to copy other files + } + } + } + + // copy files from lib + if (Directory.Exists(libFolder)) + { + // check that the lib folder has at most 1 dll + if (Directory.EnumerateFiles(libFolder).Count(file => String.Equals(Path.GetExtension(file), ".dll", StringComparison.OrdinalIgnoreCase)) > 1) + { + request.Warning(String.Format(CultureInfo.CurrentCulture, Resources.Messages.MoreThanOneDllExists, provider.Name)); + return false; + } + + foreach (string child in Directory.EnumerateFiles(libFolder)) + { + try + { + File.Copy(child, Path.Combine(versionFolder, Path.GetFileName(child)), true); + } + catch (Exception e) + { + request.Debug(e.StackTrace); + if (!(e is UnauthorizedAccessException || e is IOException)) + { + // something wrong, delete the version folder + versionFolder.TryHardToDelete(); + return false; + } + + // otherwise this means the file is just being used. so just moves on to copy other files + } + } + } + + // target file name is the assembly provider + string targetFile = Path.Combine(versionFolder, Path.GetFileName(link.Attributes[Iso19770_2.Discovery.TargetFilename])); + + if (File.Exists(targetFile)) + { + request.Verbose(Resources.Messages.InstalledPackage, provider.Name, targetFile); + request.YieldFromSwidtag(provider, fastPath); + return true; + } + } + } + finally + { + downloadedNupkg.TryHardToDelete(); + extractedFolder.TryHardToDelete(); + } + } + + return false; + } + + private bool InstallPackageFile(Package provider, string fastPath, BootstrapRequest request) { + // we can download and verify this package and get the core to install it. + var file = request.DownloadAndValidateFile(provider._swidtag); + if (file != null) { + // we have a valid file. + // run the installer + if (request.ProviderServices.Install(file, "", request)) { + // it installed ok! + request.YieldFromSwidtag(provider, fastPath); + PackageManagementService.LoadProviders(request.As()); + return true; + } + request.Warning(Constants.Messages.FailedProviderBootstrap, fastPath); + } + return false; + } + + private bool InstallPackageReference(Package provider, string fastPath, BootstrapRequest request, SoftwareIdentity[] packages) { + IHostApi installRequest = request; + if (packages[0].Provider.Name.EqualsIgnoreCase("PowerShellGet") && !request.ProviderServices.IsElevated) { + // if we're not elevated, we want powershellget to install to the user scope + + installRequest = new object[] { + new { + GetOptionKeys = new Func>(() => request.OptionKeys.ConcatSingleItem("Scope")), + GetOptionValues = new Func>((key) => { + if (key != null && key.EqualsIgnoreCase("Scope")) { + return "CurrentUser".SingleItemAsEnumerable(); + } + return request.GetOptionValues(key); + }) + } + , installRequest + }.As(); + } + + var installing = packages[0].Provider.InstallPackage(packages[0], installRequest); + + SoftwareIdentity lastPackage = null; + + foreach (var i in installing) { + lastPackage = i; + // should we echo each package back as it comes back? + request.YieldSoftwareIdentity(i.FastPackageReference, i.Name, i.Version, i.VersionScheme, i.Summary, i.Source, i.SearchKey, i.FullPath, i.PackageFilename); + + if (request.IsCanceled) { + installing.Cancel(); + } + } + + if (!request.IsCanceled && lastPackage != null) { + if (provider.Name.EqualsIgnoreCase("PowerShellGet")) { + // special case. PSModules we can just ask the PowerShell provider to pick it up + // rather than try to scan for it. + PackageManagementService.TryLoadProviderViaMetaProvider("PowerShell", lastPackage.FullPath, request); + request.YieldFromSwidtag(provider, fastPath); + return true; + } + + // looks like it installed ok. + request.YieldFromSwidtag(provider, fastPath); + + // rescan providers + PackageManagementService.LoadProviders(request.As()); + return true; + } + return false; + } + + private bool InstallAssemblyProvider(Package provider, Link link, string fastPath, BootstrapRequest request, bool deleteFile=true) { + request.Verbose(Resources.Messages.InstallingPackage, fastPath); + + if (!Directory.Exists(request.DestinationPath(request))) { + request.Error(ErrorCategory.InvalidOperation, fastPath, Constants.Messages.DestinationPathNotSet); + return false; + } + + + string targetFilename = fastPath; + string file = fastPath; + + //source can be from install-packageprovider or can be from the pipeline + if (!request.LocalSource.Any() && !fastPath.IsFile() && link != null) { + + targetFilename = link.Attributes[Iso19770_2.Discovery.TargetFilename]; + + // download the file + file = request.DownloadAndValidateFile(provider._swidtag); + + } + + if (string.IsNullOrWhiteSpace(targetFilename)) { + request.Error(ErrorCategory.InvalidOperation, fastPath, Constants.Messages.InvalidFilename); + return false; + } + + targetFilename = Path.GetFileName(targetFilename); + if (string.IsNullOrWhiteSpace(provider.Version)) { + request.Error(ErrorCategory.InvalidOperation, fastPath, Resources.Messages.MissingVersion); + return false; + } + + //the provider is installing to like this folder: \WindowsPowerShell\Modules\PackageManagement\ProviderAssemblies\nuget\2.8.5.127 + //... providername\version\.dll + var versionFolder = Path.Combine(request.DestinationPath(request), provider.Name, provider.Version); + + if (!Directory.Exists(versionFolder)) { + //we create it + Directory.CreateDirectory(versionFolder); + } + + var targetFile = Path.Combine(versionFolder, targetFilename); + + if (file != null) { + try + { + // looks good! let's keep it + if (File.Exists(targetFile)) { + request.Debug("Removing old file '{0}'", targetFile); + targetFile.TryHardToDelete(); + } + + // is that file still there? + if (File.Exists(targetFile)) { + request.Error(ErrorCategory.InvalidOperation, fastPath, Constants.Messages.UnableToRemoveFile, targetFile); + return false; + } + + request.Debug("Copying file '{0}' to '{1}'", file, targetFile); + try { + File.Copy(file, targetFile); + } + catch (Exception ex) { + request.Debug(ex.StackTrace); + return false; + } + + if (File.Exists(targetFile)) { + request.Verbose(Resources.Messages.InstalledPackage, provider.Name, targetFile); + request.YieldFromSwidtag(provider, fastPath); + + //Load the provider. This is needed when a provider has dependencies. For example, if Nuget provider has a dependent foobar + // provider and when 'Install-PackageProvider -name NuGet', we want both NuGet and Foobar provider gets loaded. + PackageManagementService.LoadProviderAssembly(request, targetFile, false); + return true; + } + + } + finally + { + if (deleteFile) { + file.TryHardToDelete(); + } + } + } + + return false; + } + + private void InstallPackageFromFile(string fastPath, BootstrapRequest request) + { + var filePath = new Uri(fastPath).LocalPath; + + var pkg = request.GetProviderFromFile(filePath, true, false); + + if (pkg != null) { + InstallAssemblyProvider(pkg, null, filePath, request, false); + } + } + + public void InstallPackage(string fastPath, BootstrapRequest request) + { + InstallPackage(fastPath, request, false); + } + + internal void InstallPackage(string fastPath, BootstrapRequest request, bool errorContinue) { + if (fastPath == null) { + throw new ArgumentNullException("fastPath"); + } + if (request == null) { + throw new ArgumentNullException("request"); + } + // ensure that mandatory parameters are present. + request.Debug("Calling 'Bootstrap::InstallPackage'"); + var triedAndFailed = false; + + //source can be from install - packageprovider or can be from the pipeline + if ((request.LocalSource.Any() || fastPath.IsFile())) + { + InstallPackageFromFile(fastPath, request); + return; + } + + // verify the package integrity (ie, check if it's digitally signed before installing) + + var provider = request.GetProvider(new Uri(fastPath)); + if (provider == null || !provider.IsValid) { + var result = errorContinue ? request.Warning(Constants.Messages.UnableToResolvePackage, fastPath) : request.Error(ErrorCategory.InvalidData, fastPath, Constants.Messages.UnableToResolvePackage, fastPath); + return; + } + + // first install the dependencies if any + var dependencyLinks = provider._swidtag.Links.Where(link => link.Relationship == Iso19770_2.Relationship.Requires).GroupBy(link => link.Artifact); + foreach (var depLinks in dependencyLinks) { + foreach (var item in depLinks) { + Package packages = null; + if (string.IsNullOrWhiteSpace(item.Attributes[Iso19770_2.Discovery.Name])) { + //select the packages that marked as "latest" + packages = (new Feed(request, new[] {item.HRef})).Query().FirstOrDefault(); + } else { + if (string.IsNullOrWhiteSpace(item.Attributes[Iso19770_2.Discovery.Version])) { + //select the packages that marked as "latest" and matches the name specified + packages = (new Feed(request, new[] { item.HRef })).Query().FirstOrDefault(p => p.Name.EqualsIgnoreCase(item.Attributes[Iso19770_2.Discovery.Name])); + } else { + //select the packages that matches version and name + packages = (new Feed(request, new[] { item.HRef })).Query(item.Attributes[Iso19770_2.Discovery.Name], item.Attributes[Iso19770_2.Discovery.Version]).FirstOrDefault(); + } + } + + if (packages == null) { + // no package found + request.Warning(Resources.Messages.NoDependencyPackageFound, item.HRef); + continue; + } + // try to install dependent providers. If fails, continue + InstallPackage(packages.Location.AbsoluteUri, request, errorContinue: true); + } + } + + // group the links along 'artifact' lines + var artifacts = provider._swidtag.Links.Where(link => link.Relationship == Iso19770_2.Relationship.InstallationMedia).GroupBy(link => link.Artifact); + + + // try one artifact set at a time. + foreach (var artifact in artifacts) { + // first time we succeed, we're good to go. + foreach (var link in artifact) { + switch (link.Attributes[Iso19770_2.Discovery.Type]) { + case "assembly": + if (InstallAssemblyProvider(provider, link, fastPath, request)) { + return; + } + triedAndFailed = true; + continue; + + default: + if (InstallProviderFromInstaller(provider, link, fastPath, request)) { + return; + } + triedAndFailed = true; + continue; + } + } + } + + if (triedAndFailed) { + // we tried installing something and it didn't go well. + var result = errorContinue ? request.Warning(Constants.Messages.FailedProviderBootstrap, fastPath) : request.Error(ErrorCategory.InvalidOperation, fastPath, Constants.Messages.FailedProviderBootstrap, fastPath); + } else { + // we didn't even find a link to bootstrap. + var result = errorContinue ? request.Warning(Resources.Messages.MissingInstallationmedia, fastPath) : request.Error(ErrorCategory.InvalidOperation, fastPath, Resources.Messages.MissingInstallationmedia, fastPath); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.CoreProviders/Bootstrap/BootstrapRequest.cs b/src/Microsoft.PackageManagement.CoreProviders/Bootstrap/BootstrapRequest.cs new file mode 100644 index 000000000..9c22673b5 --- /dev/null +++ b/src/Microsoft.PackageManagement.CoreProviders/Bootstrap/BootstrapRequest.cs @@ -0,0 +1,632 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Providers.Internal.Bootstrap { + using System; + using System.Collections; + using System.Globalization; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Xml.Linq; + using System.Security.Cryptography; + using System.Management.Automation; + using PackageManagement.Internal; + using PackageManagement.Internal.Implementation; + using PackageManagement.Internal.Packaging; + using PackageManagement.Internal.Utility.Platform; + using PackageManagement.Internal.Utility.Collections; + using PackageManagement.Internal.Utility.Extensions; + using ErrorCategory = PackageManagement.Internal.ErrorCategory; + + public abstract class BootstrapRequest : Request { + internal Uri[] _urls + { + get + { + string testfeed = Environment.GetEnvironmentVariable("BootstrapProviderTestfeedUrl"); + + // if testfeed exists, use that + if (!string.IsNullOrWhiteSpace(testfeed)) + { + Uri result = null; + + // test whether the uri is valid + if (Uri.TryCreate(testfeed, UriKind.Absolute, out result)) + { + return new Uri[] { + result + }; + } + } + + return new Uri[] { + #if LOCAL_DEBUG + new Uri("https://localhost:81/providers.swidtag"), + #endif + #if CORECLR + new Uri("https://go.microsoft.com/fwlink/?LinkID=627340&clcid=0x409"), + // starting in 2015/05 builds, we bootstrap from here: + #else + new Uri("https://go.microsoft.com/fwlink/?LinkID=627338&clcid=0x409"), + #endif + }; + } + } + + private IEnumerable _feeds; + private IEnumerable _fileSource = null; + + private IEnumerable Feeds { + get { + if (_feeds == null) { + if (LocalSource.Any()) { + Verbose(Resources.Messages.UseLocalSource, LocalSource.FirstOrDefault()); + return Enumerable.Empty(); + } + + if (!System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable()) { + Warning(Constants.Messages.NetworkNotAvailable); + Warning(string.Format(CultureInfo.CurrentCulture, Resources.Messages.ProviderBootstrapFailed)); + } + + // right now, we only have one feed (can have many urls tho') + // so we just return a single feed in the collection + // but later, we can expand it to support multiple feeds. + var feed = new Feed(this, _urls); + if (feed.IsValid) { + _feeds = feed.SingleItemAsEnumerable().ReEnumerable(); + } else { + Warning(Constants.Messages.ProviderSwidtagUnavailable); + return Enumerable.Empty(); + } + } + return _feeds; + } + } + + internal IEnumerable LocalSource { + get + { + if (_fileSource == null) + { + if (Sources.IsNullOrEmpty()) + { + _fileSource = Enumerable.Empty(); + } else { + _fileSource = Sources.Where(each => !string.IsNullOrWhiteSpace(each) && (System.IO.File.Exists(each) || System.IO.Directory.Exists(each))).WhereNotNull(); + } + } + return _fileSource; + } + } + + internal string DestinationPath(Request request) { + + var pms = PackageManagementService as PackageManagementService; + + var scope = GetValue("Scope"); + if (!string.IsNullOrWhiteSpace(scope)) { + if (scope.EqualsIgnoreCase("CurrentUser")) { + return pms.UserAssemblyLocation; + } + if (AdminPrivilege.IsElevated) { + return pms.SystemAssemblyLocation; + } else { + //a user specifies 'AllUsers' that requires Admin provilege. However his console gets launched by non-elevated. + Error(ErrorCategory.InvalidOperation, ErrorCategory.InvalidOperation.ToString(), + PackageManagement.Resources.Messages.InstallRequiresCurrentUserScopeParameterForNonAdminUser, pms.SystemAssemblyLocation, pms.UserAssemblyLocation); + return null; + } + } + + var v = GetValue("DestinationPath"); + if (String.IsNullOrWhiteSpace(v)) { + // use a well-known path. + v = AdminPrivilege.IsElevated ? pms.SystemAssemblyLocation : pms.UserAssemblyLocation; + if (String.IsNullOrWhiteSpace(v)) { + return null; + } + } + return Path.GetFullPath(v); + } + + internal IEnumerable Providers { + get { + return Feeds.SelectMany(feed => feed.Query()); + } + } + + private string GetValue(string name) { + // get the value from the request + return (GetOptionValues(name) ?? Enumerable.Empty()).LastOrDefault(); + } + + internal Package GetProvider(Uri uri) { + return new Package(this, uri.SingleItemAsEnumerable()); + } + + internal Package GetProvider(string name) { + return Feeds.SelectMany(feed => feed.Query(name)).FirstOrDefault(); + } + + internal Package GetProvider(string name, string version) { + return Feeds.SelectMany(feed => feed.Query(name, version)).FirstOrDefault(); + } + + internal IEnumerable GetProviderAll(string name, string minimumversion, string maximumversion) { + return Feeds.SelectMany(feed => feed.Query(name, minimumversion, maximumversion)); + } + + internal IEnumerable GetProvider(string name, string minimumversion, string maximumversion) { + return new[] { + GetProviderAll(name, minimumversion, maximumversion) + .OrderByDescending(each => SoftwareIdentityVersionComparer.Instance).FirstOrDefault() + }; + } + + internal string DownloadAndValidateFile(Swidtag swidtag) { + var file = DownLoadFileFromLinks(swidtag.Links.Where(each => each.Relationship == Iso19770_2.Relationship.InstallationMedia)); + if (string.IsNullOrWhiteSpace(file)) { + return null; + } + + var payload = swidtag.Payload; + if (payload == null) { + //We let the providers that are already posted in the public continue to be installed. + return file; + } else { + //validate the file hash + var valid = ValidateFileHash(file, payload); + if (!valid) { + //if the hash does not match, delete the file in the temp folder + file.TryHardToDelete(); + return null; + } + return file; + } + } + + /// + /// Helper function to retry downloading a file. + /// downloadFileFunction is the main function that is used to download the file when given a uri + /// numberOfTry is how many times we can try to download it + /// + /// + /// + /// + /// + internal string RetryDownload(Func downloadFileFunction, Uri location, uint numberOfTry = 3) { + string file = null; + + // if scheme is not https, write warning and ignores this link + if (!string.Equals(location.Scheme, "https")) { + Warning(string.Format(CultureInfo.CurrentCulture, Resources.Messages.OnlyHttpsSchemeSupported, location.AbsoluteUri)); + return file; + } + + // try 3 times to see whether we can download this + int remainingTry = 3; + + // try to download the file for remainingTry times + while (remainingTry > 0) { + try { + file = downloadFileFunction(location); + } finally { + if (file == null || !file.FileExists()) { + // file cannot be download + file = null; + remainingTry -= 1; + Verbose(string.Format(CultureInfo.CurrentCulture, Resources.Messages.RetryDownload, location.AbsoluteUri, remainingTry)); + } else { + // file downloaded, no need to retry. + remainingTry = 0; + } + } + } + + return file; + } + + + + internal string DownLoadFileFromLinks(IEnumerable links) { + string file = null; + + foreach (var link in links) { + file = RetryDownload( + // the download function takes in a uri link and download it + (uri) => { + var tmpFile = FilesystemExtensions.GenerateTemporaryFileOrDirectoryNameInTempDirectory(); + return ProviderServices.DownloadFile(uri, tmpFile, -1, true, this); + }, + link.HRef); + + // got a valid file! + if (file != null && file.FileExists()) { + return file; + } + } + + return file; + } + + private bool ValidateFileHash(string fileFullPath, Payload payload) { + + Debug("BoostrapRequest::ValidateFileHash"); + /* format: + * + * + * + */ + + if (payload == null || fileFullPath == null || !fileFullPath.FileExists()) { + return false; + } + + try { + if ((payload.Files == null) || !payload.Files.Any()) { + Error(ErrorCategory.InvalidData, "Payload", Constants.Messages.MissingFileTag); + return false; + } + var fileTag = payload.Files.FirstOrDefault(); + + if ((fileTag.Attributes == null) || (fileTag.Attributes.Keys == null)) { + Error(ErrorCategory.InvalidData, "Payload", Constants.Messages.MissingHashAttribute); + return false; + } + + var hashtag = fileTag.Attributes.Keys.FirstOrDefault(each => each.LocalName.Equals("hash")); + if (hashtag == null) { + Error(ErrorCategory.InvalidData, "Payload", Constants.Messages.MissingHashAttribute); + return false; + } + + //Note we cannot use switch here because these xname like Iso19770_2.Hash.Hash512, is not compiler time constant + string packageHash = null; + HashAlgorithm hashAlgorithm = null; + + if (hashtag.Equals(Iso19770_2.Hash.Hash512)) { + hashAlgorithm = SHA512.Create(); + packageHash = fileTag.GetAttribute(Iso19770_2.Hash.Hash512); + } else if (hashtag.Equals(Iso19770_2.Hash.Hash256)) { + hashAlgorithm = SHA256.Create(); + packageHash = fileTag.GetAttribute(Iso19770_2.Hash.Hash256); + } else if (hashtag.Equals(Iso19770_2.Hash.Md5)) { + hashAlgorithm = MD5.Create(); + packageHash = fileTag.GetAttribute(Iso19770_2.Hash.Md5); + } else { + //hash alroghtme not supported, we support 512, 256, md5 only + Error(ErrorCategory.InvalidData, "Payload", Constants.Messages.UnsupportedHashAlgorithm, hashtag, + new[] {Iso19770_2.HashAlgorithm.Sha512, Iso19770_2.HashAlgorithm.Sha256, Iso19770_2.HashAlgorithm.Md5}.JoinWithComma()); + return false; + } + + if (string.IsNullOrWhiteSpace(packageHash) || hashAlgorithm == null) { + //missing hash content? + Error(ErrorCategory.InvalidData, "Payload", Constants.Messages.MissingHashContent); + return false; + } + + // Verify the hash + using (FileStream stream = System.IO.File.OpenRead(fileFullPath)) { + // compute the hash from the file + byte[] computedHash = hashAlgorithm.ComputeHash(stream); + + try { + // convert the original hash we got from the payload tag + byte[] expectedHash = Convert.FromBase64String(packageHash); + //check if hash is equal + if (!computedHash.SequenceEqual(expectedHash)) { + // the file downloaded is not the same as expected. The file is modified. + Error(ErrorCategory.SecurityError, "Payload", Constants.Messages.HashNotEqual, packageHash, Convert.ToBase64String(computedHash)); + return false; + } + + return true; + + } catch (FormatException ex) { + Warning(ex.Message); + Error(ErrorCategory.SecurityError, "Payload", Constants.Messages.InvalidHashFormat, packageHash); + } + } + } catch (Exception ex) { + Warning(ex.Message); + } + return false; + } + + internal bool YieldFromSwidtag(Package provider, string requiredVersion, string minimumVersion, string maximumVersion, string searchKey) { + if (provider == null) { + // if the provider isn't there, just return. + return !IsCanceled; + } + + if (AnyNullOrEmpty(provider.Name, provider.Version, provider.VersionScheme)) { + Debug("Skipping yield on swid due to missing field \r\n", provider.ToString()); + return !IsCanceled; + } + + if (!String.IsNullOrWhiteSpace(requiredVersion)) { + if (provider.Version != requiredVersion) { + return !IsCanceled; + } + } else { + if (!String.IsNullOrWhiteSpace(minimumVersion) && SoftwareIdentityVersionComparer.CompareVersions(provider.VersionScheme, provider.Version, minimumVersion) < 0) { + return !IsCanceled; + } + + if (!String.IsNullOrWhiteSpace(maximumVersion) && SoftwareIdentityVersionComparer.CompareVersions(provider.VersionScheme, provider.Version, maximumVersion) > 0) { + return !IsCanceled; + } + } + return YieldFromSwidtag(provider, searchKey); + } + + + internal bool YieldFromSwidtag(Package pkg, string searchKey) { + if (pkg == null) { + return !IsCanceled; + } + + var provider = pkg._swidtag; + var fastPackageReference = LocalSource.Any() ? pkg.Location.LocalPath : pkg.Location.AbsoluteUri; + var source = pkg.Source ?? fastPackageReference; + + var summary = pkg.Name; + var targetFileName = pkg.Name; + + if (!LocalSource.Any()) { + summary = new MetadataIndexer(provider)[Iso19770_2.Attributes.Summary.LocalName].FirstOrDefault(); + targetFileName = provider.Links.Select(each => each.Attributes[Iso19770_2.Discovery.TargetFilename]).WhereNotNull().FirstOrDefault(); + } + + if (YieldSoftwareIdentity(fastPackageReference, provider.Name, provider.Version, provider.VersionScheme, summary, source, searchKey, null, targetFileName) != null) { + // yield all the meta/attributes + if (provider.Meta.Any( + m => { + var element = AddMeta(fastPackageReference); + var attributes = m.Attributes; + return attributes.Keys.Any(key => { + var nspace = key.Namespace.ToString(); + if (String.IsNullOrWhiteSpace(nspace)) { + return AddMetadata(element, key.LocalName, attributes[key]) == null; + } + + return AddMetadata(element, new Uri(nspace), key.LocalName, attributes[key]) == null; + }); + })) { + return !IsCanceled; + } + + if (provider.Links.Any(link => AddLink(link.HRef, link.Relationship, link.MediaType, link.Ownership, link.Use, link.Media, link.Artifact) == null)) { + return !IsCanceled; + } + + if (provider.Entities.Any(entity => AddEntity(entity.Name, entity.RegId, entity.Role, entity.Thumbprint) == null)) { + return !IsCanceled; + } + + //installing a package from bootstrap site needs to prompt a user. Only auto-boostrap is not prompted. + var pm = PackageManagementService as PackageManagementService; + string isTrustedSource = pm.InternalPackageManagementInstallOnly ? "false" : "true"; + if (AddMetadata(fastPackageReference, "FromTrustedSource", isTrustedSource) == null) { + return !IsCanceled; + } + } + return !IsCanceled; + } + + private static bool AnyNullOrEmpty(params string[] args) { + return args.Any(String.IsNullOrWhiteSpace); + } + + /// + /// Get a package provider from a given path. + /// + /// + /// + /// + /// + internal Package GetProviderFromFile(string filePath, bool copyFileToTemp = false, bool suppressErrorsAndWarnings = false) { + + if (string.IsNullOrWhiteSpace(filePath) && !System.IO.File.Exists(filePath)) { + Warning(Constants.Messages.FileNotFound, filePath); + return null; + } + + // support providers with .dll file extention only + if (!Path.GetExtension(filePath).EqualsIgnoreCase(".dll")) { + if (!suppressErrorsAndWarnings) + { + Warning(Resources.Messages.InvalidFileType, ".dll", filePath); + } + return null; + } + + string tempFile = filePath; + IEnumerable manifests = Enumerable.Empty(); + if (copyFileToTemp) { + try { + // Manifest.LoadFrom() does not work with network share, so we need to copy the dll to temp location + tempFile = CopyToTempLocation(filePath); + if (string.IsNullOrWhiteSpace(tempFile) && !System.IO.File.Exists(tempFile)) + { + Warning(Constants.Messages.FileNotFound, tempFile); + return null; + } + + manifests = Manifest.LoadFrom(tempFile).ToArray(); + } + finally { + if (!string.IsNullOrWhiteSpace(tempFile)) { + tempFile.TryHardToDelete(); + } + } + } else { + // providers have the provider manifest embeded? + manifests = Manifest.LoadFrom(filePath).ToArray(); + } + + if (!manifests.Any()) { + if (!suppressErrorsAndWarnings) + { + Warning(Resources.Messages.MissingProviderManifest, tempFile); + } + return null; + } + + var source = new Uri(filePath); + foreach (var manifest in manifests) { + var swidTagObject = new Swidtag(manifest); + + if (Swidtag.IsSwidtag(manifest) && swidTagObject.IsApplicable(new Hashtable())) { + + return new Package(this, swidTagObject) { + Location = source, + Source = source.LocalPath + }; + } + } + + return null; + } + + /// + /// Find a package provider from a file path. + /// + /// + /// + /// + /// + internal void FindProviderFromFile(string name, string requiredVersion, string minimumVersion, string maximumVersion) { + + // find the providers from the given Source location + var pkgs = FindProviderByNameFromFile(name).Where(each => FilterOnName(each, name) && FilterOnVersion(each, requiredVersion, minimumVersion, maximumVersion)).ReEnumerable(); + + Debug("Total {0} providers found".format(pkgs.Count())); + + //A user does not provide version info, we choose the latest + if (!GetOptionValue("AllVersions").IsTrue() && (string.IsNullOrWhiteSpace(requiredVersion) && string.IsNullOrWhiteSpace(minimumVersion) && string.IsNullOrWhiteSpace(maximumVersion))) + { + pkgs = pkgs.GroupBy(p => p.Name).Select(each => each.OrderByDescending(pp => pp.Version).FirstOrDefault()).ReEnumerable(); + } + + foreach (var package in pkgs) + { + YieldFromSwidtag(package, name); + } + } + + private IEnumerable FindProviderByNameFromFile(string name) { + foreach (var each in LocalSource) { + // each can be file full path or folder directory + var assemblies = System.IO.File.Exists(each) ? new[] {each} : System.IO.Directory.EnumerateFiles(each, "*.dll", SearchOption.AllDirectories); + + foreach (var item in assemblies) { + yield return GetProviderFromFile(item, true, false); + } + } + } + + private bool FilterOnName(Package pkg, string name) + { + if (pkg == null) + { + return false; + } + + if(string.IsNullOrWhiteSpace(name)) + { + return true; + } + + if (WildcardPattern.ContainsWildcardCharacters(name)) + { + // Applying the wildcard pattern matching + const WildcardOptions wildcardOptions = WildcardOptions.CultureInvariant | WildcardOptions.IgnoreCase; + var wildcardPattern = new WildcardPattern(name, wildcardOptions); + + return wildcardPattern.IsMatch(pkg.Name); + + } + else + { + return pkg.Name.EqualsIgnoreCase(name); + } + } + + private bool FilterOnVersion(Package pkg, string requiredVersion, string minimumVersion, string maximumVersion) { + + if(pkg == null) { + return false; + } + + if (string.IsNullOrWhiteSpace(requiredVersion) || (SoftwareIdentityVersionComparer.CompareVersions(pkg.VersionScheme, pkg.Version, requiredVersion) == 0)) + { + if (string.IsNullOrWhiteSpace(minimumVersion) || (SoftwareIdentityVersionComparer.CompareVersions(pkg.VersionScheme, pkg.Version, minimumVersion) >= 0)) + { + if (string.IsNullOrWhiteSpace(maximumVersion) || (SoftwareIdentityVersionComparer.CompareVersions(pkg.VersionScheme, pkg.Version, maximumVersion) <= 0)) + { + return true; + } + } + } + + return false; + } + + public static string GetTempFileFullPath(string filePath) { + if (string.IsNullOrWhiteSpace(filePath)) { + return filePath; + } + // get a temp location + var file = Path.Combine(Path.GetTempPath(), Path.GetFileName(filePath)); + + if (System.IO.File.Exists(file)) { + //if exists already, delete it + file.TryHardToDelete(); + } + + // is that file still there? + if (System.IO.File.Exists(file)) { + //try it again if the generated file already exists + file = GetTempFileFullPath(filePath); + } + + return file; + } + + private string CopyToTempLocation(string filePath) { + if (string.IsNullOrWhiteSpace(filePath)) { + return filePath; + } + + var targetFile = GetTempFileFullPath(filePath); + + if (filePath.EqualsIgnoreCase(targetFile)) { + return filePath; + } + + Debug("Copying file '{0}' to '{1}'", filePath, targetFile); + try { + System.IO.File.Copy(filePath, targetFile); + return targetFile; + } catch (Exception ex) { + Debug(ex.StackTrace); + return string.Empty; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.CoreProviders/Bootstrap/Feed.cs b/src/Microsoft.PackageManagement.CoreProviders/Bootstrap/Feed.cs new file mode 100644 index 000000000..3bb61d353 --- /dev/null +++ b/src/Microsoft.PackageManagement.CoreProviders/Bootstrap/Feed.cs @@ -0,0 +1,234 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Providers.Internal.Bootstrap { + using System; + using System.Collections.Generic; + using System.Linq; + using PackageManagement.Internal.Packaging; + using PackageManagement.Internal.Utility.Extensions; + + internal class Feed : Swid { + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Feed(BootstrapRequest request, Swidtag swidtag) + : base(request, swidtag) { + } + + internal Feed(BootstrapRequest request, IEnumerable mirrors) + : base(request, mirrors) { + } + + internal Feed(BootstrapRequest request, IEnumerable mirrors) + : base(request, mirrors) { + } + + /// + /// Follows the feed to find all the *declared latest* versions of packgaes + /// + /// A set of packages + internal IEnumerable Query() { + if (!IsValid) { + return Enumerable.Empty(); + } + + // first get all the packages that are marked as the latest version. + var packages = Packages.Select(packageGroup => new Package(_request, packageGroup.Where(link => link.Attributes[Iso19770_2.Discovery.Latest].IsTrue()))); + + // then follow any supplemental links to more declared latest packages. + var morePackages = More.SelectMany(nextGroup => new Feed(_request, nextGroup).Query()); + + // We do not follow to other feeds to find more, because declared latest packages should be in this feed (or a supplemental). + return packages.Concat(morePackages); + } + + /// + /// Follows the feed to find all versions of package matching 'name' + /// + /// the name or partial name of a package to find + /// A set of packages + internal IEnumerable Query(string name) { + if (!IsValid || string.IsNullOrEmpty(name)) { + return Enumerable.Empty(); + } + + // first get all the packages that are in this feed with a matched name + var packages = PackagesFilteredByName(name).Select(packageGroup => new Package(_request, packageGroup)).Where(package => package.IsValid && package.Name.EqualsIgnoreCase(name)); + + // then follow any supplemental links to more declared latest packages. + var morePackages = More.SelectMany(nextGroup => new Feed(_request, nextGroup).Query(name)); + + // let's search child feeds that declare that the name of the package in the feed matches the given name + var packagesByName = Feeds.Where(feedGroup => feedGroup.Any(link => name.EqualsIgnoreCase(link.Attributes[Iso19770_2.Discovery.Name]))).SelectMany(feed => new Feed(_request, feed).Query(name)); + + // and search child feeds that the name would be in their range. + var packagesByNameRange = Feeds.Where(feedGroup => feedGroup.Any(link => { + var minName = link.Attributes[Iso19770_2.Discovery.MinimumName]; + var maxName = link.Attributes[Iso19770_2.Discovery.MaximumName]; + if (string.IsNullOrEmpty(minName) || string.IsNullOrEmpty(maxName)) { + return false; + } + return (String.Compare(minName, name, StringComparison.OrdinalIgnoreCase) <= 0 && String.Compare(name, maxName, StringComparison.OrdinalIgnoreCase) <= 0); + })).SelectMany(feed => new Feed(_request, feed).Query(name)); + + return packages.Concat(morePackages).Concat(packagesByName).Concat(packagesByNameRange); + } + + /// + /// Follows the feed to find the specific version of a package matching 'name' + /// + /// + /// + /// A set of packages + internal IEnumerable Query(string name, string version) { + if (string.IsNullOrEmpty(version)) { + return Query(name); + } + + if (!IsValid || string.IsNullOrEmpty(name)) { + return Enumerable.Empty(); + } + + // first get all the packages that are in this feed with a matched name and version + var packages = PackagesFilteredByName(name).Select(packageGroup => new Package(_request, packageGroup)) + .Where(package => package.IsValid && package.Name.EqualsIgnoreCase(name) && SoftwareIdentityVersionComparer.CompareVersions(package.VersionScheme, package.Version, version) == 0); + + // then follow any supplemental links to more declared latest packages. + var morePackages = More.SelectMany(nextGroup => new Feed(_request, nextGroup).Query(name, version)); + + // let's search child feeds that declare that the name of the package in the feed matches the given name + // and the version is either in the specified range of the link, or there is no specified version. + var packagesByName = Feeds.Where(feedGroup => feedGroup.Any(link => { + if (name.EqualsIgnoreCase(link.Attributes[Iso19770_2.Discovery.Name])) { + var minVer = link.Attributes[Iso19770_2.Discovery.MinimumVersion]; + if (!string.IsNullOrEmpty(minVer)) { + // since we don't know the version scheme at this point, so we just have to guess. + if (SoftwareIdentityVersionComparer.CompareVersions(Iso19770_2.VersionScheme.Unknown, minVer, version) > 0) { + // the minimum version in the feed is greater than the specified version. + return false; + } + } + var maxVer = link.Attributes[Iso19770_2.Discovery.MaximumVersion]; + if (!string.IsNullOrEmpty(maxVer)) { + // since we don't know the version scheme at this point, so we just have to guess. + if (SoftwareIdentityVersionComparer.CompareVersions(Iso19770_2.VersionScheme.Unknown, version, maxVer) > 0) { + // the given version is greater than the maximum version in the feed. + return false; + } + } + return true; + } + return false; + })).SelectMany(feed => new Feed(_request, feed).Query(name, version)); + + // and search child feeds that the name would be in their range. + // (version matches have to wait till we Query() that feed, since name ranges and version ranges shouldn't be on the same link.) + var packagesByNameRange = Feeds.Where(feedGroup => feedGroup.Any(link => { + var minName = link.Attributes[Iso19770_2.Discovery.MinimumName]; + var maxName = link.Attributes[Iso19770_2.Discovery.MaximumName]; + if (string.IsNullOrEmpty(minName) || string.IsNullOrEmpty(maxName)) { + return false; + } + return (String.Compare(minName, name, StringComparison.OrdinalIgnoreCase) <= 0 && String.Compare(name, maxName, StringComparison.OrdinalIgnoreCase) <= 0); + })).SelectMany(feed => new Feed(_request, feed).Query(name, version)); + + return packages.Concat(morePackages).Concat(packagesByName).Concat(packagesByNameRange); + } + + /// + /// Follows the feed to find the all versions of a package matching 'name', in the given range + /// + /// + /// + /// + /// A set of packages + internal IEnumerable Query(string name, string minimumVersion, string maximumVersion) { + if (string.IsNullOrEmpty(minimumVersion) && string.IsNullOrEmpty(maximumVersion)) { + return Query(name); + } + + if (!IsValid || string.IsNullOrEmpty(name)) { + return Enumerable.Empty(); + } + + // first get all the packages that are in this feed with a matched name and version + var packages = PackagesFilteredByName(name).Select(packageGroup => new Package(_request, packageGroup)).Where(package => { + if (package.IsValid && package.Name.EqualsIgnoreCase(name)) { + if (!string.IsNullOrWhiteSpace(minimumVersion)) { + if (SoftwareIdentityVersionComparer.CompareVersions(package.VersionScheme, package.Version, minimumVersion) < 0) { + // a minimum version was specified, but the package version is less than the specified minimumversion. + return false; + } + } + + if (!string.IsNullOrWhiteSpace(maximumVersion)) { + if (SoftwareIdentityVersionComparer.CompareVersions(package.VersionScheme, package.Version, maximumVersion) > 0) { + // a maximum version was specified, but the package version is more than the specified maximumversion. + return false; + } + } + + // the version is in the range asked for. + return true; + } + // not a valid package, or incorrect name. + return false; + }); + + // then follow any supplemental links to more declared latest packages. + var morePackages = More.SelectMany(nextGroup => new Feed(_request, nextGroup).Query(name, minimumVersion, maximumVersion)); + + // let's search child feeds that declare that the name of the package in the feed matches the given name + // and the version is either in the specified range of the link, or there is no specified version. + var packagesByName = Feeds.Where(feedGroup => feedGroup.Any(link => { + if (name.EqualsIgnoreCase(link.Attributes[Iso19770_2.Discovery.Name])) { + // first, ensure that the requested miniumum version is lower than the maximum version found in the feed. + var maxVer = link.Attributes[Iso19770_2.Discovery.MaximumVersion]; + if (!string.IsNullOrEmpty(maxVer)) { + // since we don't know the version scheme at this point, so we just have to guess. + if (SoftwareIdentityVersionComparer.CompareVersions(Iso19770_2.VersionScheme.Unknown, minimumVersion, maxVer) <= 0) { + // the minimum version is greater than the maximum version in the feed. + return false; + } + } + + // and then ensure that the requested maximum version is greater than the miniumum version found in the feed. + var minVer = link.Attributes[Iso19770_2.Discovery.MinimumVersion]; + if (!string.IsNullOrEmpty(minVer)) { + // since we don't know the version scheme at this point, so we just have to guess. + if (SoftwareIdentityVersionComparer.CompareVersions(Iso19770_2.VersionScheme.Unknown, maximumVersion, minVer) >= 0) { + // the maximum version less than the minimum version in the feed. + return false; + } + } + + return true; + } + return false; + })).SelectMany(feed => new Feed(_request, feed).Query(name, minimumVersion, maximumVersion)); + + // and search child feeds that the name would be in their range. + // (version matches have to wait till we Query() that feed, since name ranges and version ranges shouldn't be on the same link.) + var packagesByNameRange = Feeds.Where(feedGroup => feedGroup.Any(link => { + var minName = link.Attributes[Iso19770_2.Discovery.MinimumName]; + var maxName = link.Attributes[Iso19770_2.Discovery.MaximumName]; + if (string.IsNullOrEmpty(minName) || string.IsNullOrEmpty(maxName)) { + return false; + } + return (String.Compare(minName, name, StringComparison.OrdinalIgnoreCase) <= 0 && String.Compare(name, maxName, StringComparison.OrdinalIgnoreCase) <= 0); + })).SelectMany(feed => new Feed(_request, feed).Query(name, minimumVersion, maximumVersion)); + + return packages.Concat(morePackages).Concat(packagesByName).Concat(packagesByNameRange); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.CoreProviders/Bootstrap/Package.cs b/src/Microsoft.PackageManagement.CoreProviders/Bootstrap/Package.cs new file mode 100644 index 000000000..550648c67 --- /dev/null +++ b/src/Microsoft.PackageManagement.CoreProviders/Bootstrap/Package.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Providers.Internal.Bootstrap { + using System; + using System.Collections.Generic; + using PackageManagement.Internal.Packaging; + using Packaging; + + internal class Package : Swid { + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Package(BootstrapRequest request, Swidtag swidtag) + : base(request, swidtag) { + } + + internal Package(BootstrapRequest request, IEnumerable mirrors) : base(request, mirrors) { + } + + internal Package(BootstrapRequest request, IEnumerable mirrors) + : base(request, mirrors) { + } + + internal string Name { + get { + if (IsValid) { + return _swidtag.Name; + } + return string.Empty; + } + } + + internal string Version { + get { + if (IsValid) { + return _swidtag.Version ?? "0"; + } + return "0"; + } + } + + internal string VersionScheme { + get { + if (IsValid) { + return _swidtag.VersionScheme ?? Iso19770_2.VersionScheme.Unknown; + } + return Iso19770_2.VersionScheme.Unknown; + } + } + + internal string Source {get; set;} + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.CoreProviders/Bootstrap/Swid.cs b/src/Microsoft.PackageManagement.CoreProviders/Bootstrap/Swid.cs new file mode 100644 index 000000000..1a68b7623 --- /dev/null +++ b/src/Microsoft.PackageManagement.CoreProviders/Bootstrap/Swid.cs @@ -0,0 +1,175 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Providers.Internal.Bootstrap { + using System; + using System.Globalization; + using System.Collections.Generic; + using System.Linq; + using System.Xml.Linq; + using PackageManagement.Internal; + using PackageManagement.Internal.Api; + using PackageManagement.Internal.Implementation; + using PackageManagement.Internal.Packaging; + using PackageManagement.Internal.Utility.Extensions; + using PackageManagement.Internal.Utility.Plugin; + + internal class Swid { + private const int SwidDownloadTimeout = 40000; + internal readonly BootstrapRequest _request; + internal readonly Swidtag _swidtag; + private bool _timedOut; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Swid(BootstrapRequest request, Swidtag swidtag) { + _request = request; + _swidtag = swidtag; + } + + internal Swid(BootstrapRequest request, IEnumerable mirrors) { + _request = request; + _swidtag = DownloadSwidtag(mirrors); + } + + internal Swid(BootstrapRequest request, IEnumerable mirrors) { + _request = request; + _swidtag = DownloadSwidtag(mirrors); + } + + internal Uri Location {get; set;} + + private IRequest DownloadRequest { + get { + _timedOut = false; + // overrides the Warning to check for timed out messages. + return new object[] { + new { + Warning = new Func((messageText) => { + if (messageText == Constants.Status.TimedOut) { + _timedOut = true; + return true; + } + return _request.Warning(messageText); + }), + Error = new Func((category, id, target, messageText ) => { + // tag download errors don't count as "errors", at best, just a warning. + return _request.Verbose(messageText); + }) + }, + _request + }.As(); + } + } + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + protected IEnumerable> Artifacts { + get { + return IsValid ? _swidtag.Links.GroupBy(link => string.IsNullOrEmpty(link.Artifact) ? FilesystemExtensions.GenerateTemporaryFileOrDirectoryNameInTempDirectory() : link.Artifact) : Enumerable.Empty>(); + } + } + + protected IEnumerable> Feeds { + get { + return IsValid + ? _swidtag.Links.Where(link => link.Relationship == Iso19770_2.Relationship.Feed).GroupBy(link => string.IsNullOrEmpty(link.Artifact) ? FilesystemExtensions.GenerateTemporaryFileOrDirectoryNameInTempDirectory() : link.Artifact) : Enumerable.Empty>(); + } + } + + protected IEnumerable> Packages { + get { + return IsValid + ? _swidtag.Links.Where(link => link.Relationship == Iso19770_2.Relationship.Package).GroupBy(link => string.IsNullOrEmpty(link.Artifact) ? FilesystemExtensions.GenerateTemporaryFileOrDirectoryNameInTempDirectory() : link.Artifact) + : Enumerable.Empty>(); + } + } + + protected IEnumerable> More { + get { + return _swidtag.Links.Where(link => link.Relationship == Iso19770_2.Relationship.Supplemental).GroupBy(link => string.IsNullOrEmpty(link.Artifact) ? FilesystemExtensions.GenerateTemporaryFileOrDirectoryNameInTempDirectory() : link.Artifact); + } + } + + internal virtual bool IsValid { + get { + return _swidtag != null; + } + } + + private Swidtag DownloadSwidtag(IEnumerable locations) { + foreach (var location in locations.WhereNotNull()) { + try { + var filename = FilesystemExtensions.GenerateTemporaryFileOrDirectoryNameInTempDirectory(); + DownloadSwidtagToFile(filename, location); + + if (_timedOut) { + // try one more time... + DownloadSwidtagToFile(filename, location); + } + + if (filename.FileExists()) { + try + { + var document = XDocument.Load(filename); + if (Swidtag.IsSwidtag(document.Root)) + { + Location = location; + return new Swidtag(document); + } + } + finally + { + System.IO.File.Delete(filename); + } + } + } catch (Exception e) { + e.Dump(); + } + } + return null; + } + + private string DownloadSwidtagToFile(string filename, Uri location) { + return _request.RetryDownload( + (uri) => + { + if (_request.ProviderServices == null) + { + // during initialization, the pluggable downloader isn't available. + // luckily, it's built into this assembly, so we'll create and use it directly. + return new WebDownloader().DownloadFile(location, filename, SwidDownloadTimeout, false, DownloadRequest.As()); + } + + // otherwise, we can just use the pluggable one. + return _request.ProviderServices.DownloadFile(location, filename, SwidDownloadTimeout, false, DownloadRequest); + }, + location + ); + } + + internal Swidtag DownloadSwidtag(IEnumerable locations) { + return DownloadSwidtag(locations.Where(link => link != null && link.HRef != null).Select(link => link.HRef)); + } + + protected IEnumerable> PackagesFilteredByName(string name) { + if (string.IsNullOrWhiteSpace(name)) { + return Packages; + } + + return Packages.Where(packageGroup => { + var n = packageGroup.FirstOrDefault().Attributes[Iso19770_2.Discovery.Name]; + return (string.IsNullOrEmpty(n) || name.EqualsIgnoreCase(n)); + }); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.CoreProviders/ProgramsProvider.cs b/src/Microsoft.PackageManagement.CoreProviders/ProgramsProvider.cs new file mode 100644 index 000000000..b34fdd804 --- /dev/null +++ b/src/Microsoft.PackageManagement.CoreProviders/ProgramsProvider.cs @@ -0,0 +1,230 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Providers.Internal { + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Management.Automation; + using System.Security.AccessControl; + using PackageManagement.Internal; + using PackageManagement.Internal.Implementation; + using PackageManagement.Internal.Packaging; + using PackageManagement.Internal.Utility.Platform; + using PackageManagement.Internal.Utility.Extensions; + using Win32; + using File = System.IO.File; + + public class ProgramsProvider { + /// + /// The name of this Package Provider + /// + internal const string ProviderName = "Programs"; + + private static readonly Dictionary _features = new Dictionary(); + + /// + /// Returns the name of the Provider. + /// + /// The name of this proivder (uses the constant declared at the top of the class) + public string GetPackageProviderName() { + return ProviderName; + } + + /// + /// Performs one-time initialization of the PROVIDER. + /// + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void InitializeProvider(Request request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::InitializeProvider'", ProviderName); + } + + /// + /// Returns a collection of strings to the client advertizing features this provider supports. + /// + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void GetFeatures(Request request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::GetFeatures' ", ProviderName); + + foreach (var feature in _features) { + request.Yield(feature); + } + } + + /// + /// Returns dynamic option definitions to the HOST + /// + /// The category of dynamic options that the HOST is interested in + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void GetDynamicOptions(string category, Request request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + category = category ?? string.Empty; + + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::GetDynamicOptions' '{1}'", ProviderName, category); + + switch (category.ToLowerInvariant()) { + case "install": + // options required for install/uninstall/getinstalledpackages + request.YieldDynamicOption("IncludeWindowsInstaller", "Switch", false); + request.YieldDynamicOption("IncludeSystemComponent", "Switch", false); + break; + + case "provider": + // options used with this provider. Not currently used. + break; + + case "source": + // options for package sources + break; + + case "package": + // options used when searching for packages + break; + } + } + + + /// + /// Returns the packages that are installed + /// + /// the package name to match. Empty or null means match everything + /// the specific version asked for. If this parameter is specified (ie, not null or empty string) then the minimum and maximum values are ignored + /// the minimum version of packages to return . If the requiredVersion parameter is specified (ie, not null or empty string) this should be ignored + /// the maximum version of packages to return . If the requiredVersion parameter is specified (ie, not null or empty string) this should be ignored + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void GetInstalledPackages(string name, string requiredVersion, string minimumVersion, string maximumVersion, Request request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::GetInstalledPackages' '{1}','{2}','{3}','{4}'", ProviderName, name, requiredVersion, minimumVersion, maximumVersion); + + // dump out results. + + if (Environment.Is64BitOperatingSystem) { + using (var hklm64 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64).OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey)) { + if (!YieldPackages("hklm64", hklm64, name, requiredVersion, minimumVersion, maximumVersion, request)) { + return; + } + } + + using (var hkcu64 = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64).OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", false)) { + if (!YieldPackages("hkcu64", hkcu64, name, requiredVersion, minimumVersion, maximumVersion, request)) { + return; + } + } + } + + using (var hklm32 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", false)) { + if (!YieldPackages("hklm32", hklm32, name, requiredVersion, minimumVersion, maximumVersion, request)) { + return; + } + } + + using (var hkcu32 = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", false)) { + if (!YieldPackages("hkcu32", hkcu32, name, requiredVersion, minimumVersion, maximumVersion, request)) { + } + } + } + + + private bool YieldPackages(string hive, RegistryKey regkey, string name, string requiredVersion, string minimumVersion, string maximumVersion, Request request) { + if (regkey != null) { + var includeWindowsInstaller = request.GetOptionValue("IncludeWindowsInstaller").IsTrue(); + var includeSystemComponent = request.GetOptionValue("IncludeSystemComponent").IsTrue(); + + var wildcardPattern = new WildcardPattern(name??"", WildcardOptions.CultureInvariant | WildcardOptions.IgnoreCase); + + + foreach (var key in regkey.GetSubKeyNames()) { + var subkey = regkey.OpenSubKey(key); + if (subkey != null) { + var properties = subkey.GetValueNames().ToDictionaryNicely(each => each.ToString(), each => (subkey.GetValue(each) ?? string.Empty).ToString(), StringComparer.OrdinalIgnoreCase); + + if (!includeWindowsInstaller && properties.ContainsKey("WindowsInstaller") && properties["WindowsInstaller"] == "1") { + continue; + } + + if (!includeSystemComponent && properties.ContainsKey("SystemComponent") && properties["SystemComponent"] == "1") { + continue; + } + + var productName = ""; + + if (!properties.TryGetValue("DisplayName", out productName)) { + // no product name? + continue; + } + + if (!string.IsNullOrWhiteSpace(productName) && (string.IsNullOrWhiteSpace(name) || wildcardPattern.IsMatch(productName))) { + + var productVersion = properties.Get("DisplayVersion") ?? ""; + var publisher = properties.Get("Publisher") ?? ""; + var uninstallString = properties.Get("QuietUninstallString") ?? properties.Get("UninstallString") ?? ""; + var comments = properties.Get("Comments") ?? ""; + + var fp = hive + @"\" + subkey; + + if (!string.IsNullOrEmpty(requiredVersion)) { + if (SoftwareIdentityVersionComparer.CompareVersions("unknown", requiredVersion, productVersion) != 0) { + continue; + } + } else { + if (!string.IsNullOrEmpty(minimumVersion) && SoftwareIdentityVersionComparer.CompareVersions("unknown", productVersion, minimumVersion) < 0) { + continue; + } + if (!string.IsNullOrEmpty(maximumVersion) && SoftwareIdentityVersionComparer.CompareVersions("unknown", productVersion, maximumVersion) > 0) { + continue; + } + } + + if (request.YieldSoftwareIdentity(fp, productName, productVersion, "unknown", comments, "", name, "", "") != null) { + if (properties.Keys.Where(each => !string.IsNullOrWhiteSpace(each)).Any(k => request.AddMetadata(fp, k.MakeSafeFileName(), properties[k]) == null)) { + return false; + } + } + } + } + } + } + return true; + } + } +} diff --git a/src/Microsoft.PackageManagement.CoreProviders/Properties/AssemblyInfo.cs b/src/Microsoft.PackageManagement.CoreProviders/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..f1e5c8a6d --- /dev/null +++ b/src/Microsoft.PackageManagement.CoreProviders/Properties/AssemblyInfo.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. + +[assembly: AssemblyTitle("PackageManagement.CoreProviders")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("CoreProviders")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM + +[assembly: Guid("cbb49c87-7eb3-4054-acfe-783d13dd9755")] \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.CoreProviders/WebDownloader.cs b/src/Microsoft.PackageManagement.CoreProviders/WebDownloader.cs new file mode 100644 index 000000000..2eff851fc --- /dev/null +++ b/src/Microsoft.PackageManagement.CoreProviders/WebDownloader.cs @@ -0,0 +1,278 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Providers.Internal +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.IO; + using System.Net; + using System.Globalization; +#if CORECLR + using System.Net.Http; +#endif + using System.Threading; + using System.Threading.Tasks; + using PackageManagement.Internal; + using PackageManagement.Internal.Implementation; + using PackageManagement.Internal.Utility.Extensions; + + public class WebDownloader { + internal static string ProviderName = "WebDownloader"; + + private static readonly Dictionary _features = new Dictionary { + {Constants.Features.SupportedSchemes, new[] {"http", "https", "ftp", "file"}}, + }; + + /// + /// Returns a collection of strings to the client advertizing features this provider supports. + /// + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void GetFeatures(Request request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::GetFeatures' ", ProviderName); + foreach (var feature in _features) { + request.Yield(feature); + } + } + + public void InitializeProvider(Request request) { + } + + public string GetDownloaderName() { + return ProviderName; + } + + public string DownloadFile(Uri remoteLocation, string localFilename, int timeoutMilliseconds, bool showProgress, Request request) { + + if (request == null) { + throw new ArgumentNullException("request"); + } + + if (remoteLocation == null) { + throw new ArgumentNullException("remoteLocation"); + } + + request.Debug("Calling 'WebDownloader::DownloadFile' '{0}','{1}','{2}','{3}'", remoteLocation, localFilename, timeoutMilliseconds, showProgress); + + if (remoteLocation.Scheme.ToLowerInvariant() != "http" && remoteLocation.Scheme.ToLowerInvariant() != "https" && remoteLocation.Scheme.ToLowerInvariant() != "ftp") { + request.Error(ErrorCategory.InvalidResult, remoteLocation.ToString(), Constants.Messages.SchemeNotSupported, remoteLocation.Scheme); + return null; + } + + if (localFilename == null) { + localFilename = FilesystemExtensions.GenerateTemporaryFileOrDirectoryNameInTempDirectory(); + } + + localFilename = Path.GetFullPath(localFilename); + + // did the caller pass us a directory name? + if (Directory.Exists(localFilename)) { + localFilename = Path.Combine(localFilename, "downloadedFile.tmp"); + } + + // make sure that the parent folder is created first. + var folder = Path.GetDirectoryName(localFilename); + if (!Directory.Exists(folder)) { + Directory.CreateDirectory(folder); + } + + // clobber an existing file if it's already there. + // todo: in the future, we could check the md5 of the file and if the remote server supports it + // todo: we could skip the download. + if (File.Exists(localFilename)) { + localFilename.TryHardToDelete(); + } + + // setup the progress tracker if the caller wanted one. + int pid = 0; + if (showProgress) { + pid = request.StartProgress(0, "Downloading '{0}'", remoteLocation); + } + +#if CORECLR + var task = Download(remoteLocation, localFilename, showProgress, request, pid); + + task.Wait(timeoutMilliseconds); + + if (!task.IsCompleted) + { + request.Warning(Constants.Status.TimedOut); + request.Debug("Timed out downloading '{0}'", remoteLocation.AbsoluteUri); + } +#else + var webClient = new WebClient(); + + // Mozilla/5.0 is the general token that says the browser is Mozilla compatible, and is common to almost every browser today. + webClient.Headers.Add("User-Agent", "Mozilla/5.0 PackageManagement"); + + if (request.WebProxy != null) + { + // if user supplies webproxy, use that + webClient.Proxy = request.WebProxy; + } + else + { + // get ie settings + webClient.Proxy = WebRequest.GetSystemWebProxy(); + + // set credentials to be user credentials + webClient.Proxy.Credentials = CredentialCache.DefaultNetworkCredentials; + } + + var done = new ManualResetEvent(false); + + webClient.DownloadFileCompleted += (sender, args) => { + if (args.Cancelled || args.Error != null) { + localFilename = null; + } + done.Set(); + }; + + var lastPercent = 0; + + if (showProgress) { + webClient.DownloadProgressChanged += (sender, args) => { + // Progress(requestObject, 2, (int)percent, "Downloading {0} of {1} bytes", args.BytesReceived, args.TotalBytesToReceive); + var percent = (int)((args.BytesReceived*100)/args.TotalBytesToReceive); + if (percent > lastPercent) { + lastPercent = percent; + request.Progress(pid, (int)((args.BytesReceived*100)/args.TotalBytesToReceive), "To {0}", localFilename); + } + }; + } + + // start the download + webClient.DownloadFileAsync(remoteLocation, localFilename); + + // wait for the completion + if (timeoutMilliseconds > 0) { + if (!done.WaitOne(timeoutMilliseconds)) + { + webClient.CancelAsync(); + request.Warning(Constants.Status.TimedOut); + request.Debug("Timed out downloading '{0}'", remoteLocation.AbsoluteUri); + return null; + } + } else { + // wait until it completes or fails on it's own + done.WaitOne(); + } + +#endif + + // if we don't have the file by this point, we've failed. + if (localFilename == null || !File.Exists(localFilename)) { + request.CompleteProgress(pid, false); + request.Warning(Constants.Messages.UnableToDownload, remoteLocation.ToString(), localFilename); + return null; + } + + if (showProgress) { + request.CompleteProgress(pid, true); + } + + return localFilename; + } + +#if CORECLR + private async Task Download(Uri remoteLocation, string localFilename, bool showProgress, Request request, int pid) + { + var clientHandler = new HttpClientHandler(); + + clientHandler.UseDefaultCredentials = true; + + // if user supplies web proxy, use that + if (request.WebProxy != null) + { + clientHandler.Proxy = request.WebProxy; + } + + var httpClient = new HttpClient(clientHandler); + + request.Debug("Calling httpclient with remotelocation {0}", remoteLocation.AbsoluteUri); + + // Mozilla/5.0 is the general token that says the browser is Mozilla compatible, and is common to almost every browser today. + httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 PackageManagement"); + httpClient.DefaultRequestHeaders.ExpectContinue = false; + + long totalBytesToReceive = 0L; + + HttpResponseMessage response = await httpClient.GetAsync(remoteLocation, HttpCompletionOption.ResponseHeadersRead); + + if (!response.IsSuccessStatusCode) + { + return null; + } + + if (response.Content != null && response.Content.Headers != null) + { + totalBytesToReceive = response.Content.Headers.ContentLength ?? 0; + } + + try + { + Stream input = await response.Content.ReadAsStreamAsync(); + byte[] bytes = new byte[1024 * 4]; + FileStream output = File.Open(localFilename, FileMode.OpenOrCreate); + + long totalDownloaded = 0; + int current = 0; + int lastPercent = 0; + + current = await input.ReadAsync(bytes, 0, bytes.Length); + + while (current > 0) + { + totalDownloaded += current; + + await output.WriteAsync(bytes, 0, current); + + if (showProgress && totalBytesToReceive != 0) + { + int percent = (int)((totalDownloaded * 100) / totalBytesToReceive); + + if (percent > lastPercent) + { + lastPercent = percent; + request.Progress(pid, (int)percent, "To {0}", localFilename); + } + } + + current = await input.ReadAsync(bytes, 0, bytes.Length); + } + + input.Dispose(); + output.Dispose(); + } + catch (Exception e) + { + request.Debug(e.Message); + localFilename = null; + } + + return localFilename; + } + +#endif + } +} diff --git a/src/Microsoft.PackageManagement.CoreProviders/provider.manifest b/src/Microsoft.PackageManagement.CoreProviders/provider.manifest new file mode 100644 index 000000000..4b54e544c --- /dev/null +++ b/src/Microsoft.PackageManagement.CoreProviders/provider.manifest @@ -0,0 +1,8 @@ + + + + diff --git a/src/Microsoft.PackageManagement.CoreProviders/resources/Microsoft.PackageManagement.Providers.Resources.Messages.resx b/src/Microsoft.PackageManagement.CoreProviders/resources/Microsoft.PackageManagement.Providers.Resources.Messages.resx new file mode 100644 index 000000000..e39083f72 --- /dev/null +++ b/src/Microsoft.PackageManagement.CoreProviders/resources/Microsoft.PackageManagement.Providers.Resources.Messages.resx @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Fail to extract and copy to '{0}'. + 0 - The directory we are copying to + + + Finding the package '{0}'. + 0 - package name + + + Installed the package '{0}' to '{1}'. + 0 - package name; 1 - file directory + + + Installing the package '{0}'. + 0 - package name + + + Install-PackageProvider -Source only supports '{0}' for file-based installation. '{1}' is not a supported file type. To install a PowerShell-based provider, you must install it from a PSRepository. + + + Provider '{0}' missing installationmedia to install. + + + '{0}' does not contain a package provider manifest. Refer to 'Get-Help Import-PackageProvider' for guidance. + + + The version is not found for the specified provider '{0}'. + 0 - package name + + + More than 1 dll with provider manifest exists in provider '{0}'. + 0 - package name + + + Cannot find dependency packages from link '{0}'. + 0 - uri link + + + Link '{0}' is not using https. + 0 - Uri link + + + Unable to bootstrap the required package provider due to problems with network connectivity. Please fix your network connection. If this is not possible, refer to 'Get-Help Install-PackageProvider' or http://go.microsoft.com/fwlink/?LinkId=626941 for guidance on installing the package provider manually. + + + Cannot download link '{0}', retrying for '{1}' more times. + 0 - Uri link, 1 - number of retries + + + Searching for providers in the file path {0}. + 0 - file folder or file name + + \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Constants.cs b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Constants.cs new file mode 100644 index 000000000..c62c21622 --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Constants.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.MetaProvider.PowerShell.Internal { + internal static class Constants { + internal const string ScriptStackTrace = "MSG:ScriptStackTrace"; + internal const string ProviderNameUnknown = "(unknown)"; + } +} diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Dependency.cs b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Dependency.cs new file mode 100644 index 000000000..e5a1f3e49 --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Dependency.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.MetaProvider.PowerShell { + public class Dependency { + public string ProviderName {get; set;} + public string PackageName {get; set;} + public string Version {get; set;} + public string Source {get; set;} + public string AppliesTo {get; set;} + } +} diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/DynamicOption.cs b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/DynamicOption.cs new file mode 100644 index 000000000..a8eb7a64a --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/DynamicOption.cs @@ -0,0 +1,61 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.MetaProvider.PowerShell { + using System; + using System.Collections.Generic; + using System.Linq; + using Internal; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + + public class DynamicOption : Yieldable { + public DynamicOption(OptionCategory category, string name, OptionType expectedType, bool isRequired, IEnumerable permittedValues) { + Name = name; + ExpectedType = expectedType; + IsRequired = isRequired; + PermittedValues = permittedValues; + } + + public DynamicOption(string name, OptionType expectedType, bool isRequired, IEnumerable permittedValues) { + Name = name; + ExpectedType = expectedType; + IsRequired = isRequired; + PermittedValues = permittedValues; + } + + + public DynamicOption(OptionCategory category,string name, OptionType expectedType, bool isRequired) : this(category, name , expectedType, isRequired, null) { + } + + public DynamicOption(string name, OptionType expectedType, bool isRequired) + : this(name, expectedType, isRequired, null) { + } + + + public DynamicOption() { + } + + public string Name {get; set;} + public OptionType ExpectedType {get; set;} + public bool IsRequired {get; set;} + public IEnumerable PermittedValues {get; set;} + + public override bool YieldResult(PsRequest r) { + if (r == null) { + throw new ArgumentNullException("r"); + } + return r.YieldDynamicOption(Name, ExpectedType.ToString(), IsRequired) && PermittedValues.WhereNotNull().Select(each => each.ToString()).ToArray().All(v => r.YieldKeyValuePair(Name, v)); + } + } +} diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Entity.cs b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Entity.cs new file mode 100644 index 000000000..e971c672e --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Entity.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.MetaProvider.PowerShell { + using System; + using System.Collections.Generic; + using System.Linq; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + + public class Entity { + public string Name {get; set;} + public string RegId {get; set;} + public string Role {get; set;} + + public IEnumerable Roles { + get { + return (string.IsNullOrWhiteSpace(Role) ? "unknown" : Role).Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries); + } + set + { + var values = value == null ? new string[0] : value.ToArray(); + Role = ( values.Length == 0) ? "unknown" : value.JoinWith(" "); + } + } + + public string Thumbprint {get; set;} + } +} diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Enums.cs b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Enums.cs new file mode 100644 index 000000000..92f281828 --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Enums.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.MetaProvider.PowerShell { + public enum OptionCategory { + Package = 0, + Provider = 1, + Source = 2, + Install = 3 + } + + public enum OptionType { + String = 0, + StringArray = 1, + Int = 2, + Switch = 3, + Folder = 4, + File = 5, + Path = 6, + Uri = 7, + SecureString = 8 + } +} diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Feature.cs b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Feature.cs new file mode 100644 index 000000000..9bc49c201 --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Feature.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.MetaProvider.PowerShell { + using System; + using System.Collections.Generic; + using System.Linq; + using Internal; + + public class Feature : Yieldable { + private KeyValuePair _pair; + + public Feature(string name) : this(name, new string[0]) { + } + + public Feature(string name, string value) : this(name, new[] {value}) { + } + + public Feature(string name, string[] values) { + _pair = new KeyValuePair(name, values); + } + + public override bool YieldResult(PsRequest r) { + if (r == null) { + throw new ArgumentNullException("r"); + } + if (_pair.Value.Length == 0) { + return r.YieldKeyValuePair(_pair.Key, null); + } + return _pair.Value.All(each => r.YieldKeyValuePair(_pair.Key, each)); + } + } +} diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Link.cs b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Link.cs new file mode 100644 index 000000000..d968f9263 --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Link.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.MetaProvider.PowerShell { + public class Link { + public string HRef {get; set;} + public string Relationship {get; set;} + public string MediaType {get; set;} + public string Ownership {get; set;} + public string Use {get; set;} + public string AppliesToMedia {get; set;} + public string Artifact {get; set;} + } +} diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/PackageSource.cs b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/PackageSource.cs new file mode 100644 index 000000000..5324c111a --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/PackageSource.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.MetaProvider.PowerShell { + using System; + using System.Collections; + using Internal; + + public class PackageSource : Yieldable { + public PackageSource(string name, string location, bool isTrusted, bool isRegistered, bool isValidated) { + Name = name; + Location = location; + IsTrusted = isTrusted; + IsRegistered = isRegistered; + IsValidated = isValidated; + } + + public PackageSource(string name, string location, bool isTrusted, bool isRegistered, bool isValidated, Hashtable details) + : this(name, location, isTrusted, isRegistered, isValidated) { + _details = details; + } + + + public PackageSource(string name, string location, bool isTrusted, bool isRegistered, Hashtable details) + : this(name, location, isTrusted, isRegistered,false) { + _details = details; + } + + public string Name {get; set;} + public string Location {get; set;} + public bool IsTrusted {get; set;} + public bool IsRegistered { get; set; } + + public bool IsValidated {get; set;} + public override bool YieldResult(PsRequest r) { + if (r == null) { + throw new ArgumentNullException("r"); + } + + return r.YieldPackageSource(Name, Location, IsTrusted, IsRegistered,IsValidated) && YieldDetails(r); + } + } +} diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/PowerShellExtensions.cs b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/PowerShellExtensions.cs new file mode 100644 index 000000000..086ba723e --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/PowerShellExtensions.cs @@ -0,0 +1,182 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Microsoft.PackageManagement.Internal.Utility.Platform; + +namespace Microsoft.PackageManagement.MetaProvider.PowerShell.Internal { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Management.Automation; + using System.Threading; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Internal.Utility.Platform; + + internal static class PowerShellExtensions { + internal static PowerShell Clear(this PowerShell powershell) { + if (powershell != null) { + powershell.WaitForReady(); + powershell.Commands = new PSCommand(); + } + return powershell; + } + + internal static PSModuleInfo ImportModule(this PowerShell powershell, string name, bool force = false) { + if (powershell != null) { + powershell.Clear().AddCommand("Import-Module"); + powershell.AddParameter("Name", name); + powershell.AddParameter("PassThru"); + + if (force) { + powershell.AddParameter("Force"); + } + return powershell.Invoke().ToArray().FirstOrDefault(); + } + return null; + } + + internal static IEnumerable TestModuleManifest(this PowerShell powershell, string path) { + if (powershell != null) { + return powershell + .Clear() + .AddCommand("Test-ModuleManifest") + .AddParameter("Path", path) + .Invoke().ToArray(); + } + return Enumerable.Empty(); + } + + internal static IEnumerable GetModule(this PowerShell powershell, string moduleName) + { + if ((powershell != null) && (!string.IsNullOrWhiteSpace(moduleName))) + { + return powershell + .Clear() + .AddCommand("Get-Module") + .AddParameter("Name", moduleName) + .AddParameter("ListAvailable") + .Invoke(); + } + return Enumerable.Empty(); + } + + internal static PowerShell SetVariable(this PowerShell powershell, string variable, object value) { + if (powershell != null) { + powershell + .Clear() + .AddCommand("Set-Variable") + .AddParameter("Name", variable) + .AddParameter("Value", value) + .Invoke(); + } + return powershell; + } + + /// + /// Return the last output to the user. This will be null if the operation did not succeed. + /// outputAction is the action that will be performed whenever an object is outputted to the powershell pipeline + /// + /// + /// + /// + /// + /// + /// + /// + internal static T InvokeFunction(this PowerShell powershell, string command, + EventHandler outputAction, EventHandler errorAction, params object[] args) { + + if (powershell != null) { + powershell.Clear().AddCommand(command); + foreach (var arg in args) { + powershell.AddArgument(arg); + } +#if DEBUG + NativeMethods.OutputDebugString("[Cmdlet:debugging] -- InvokeFunction ({0}, {1})".format(command, args.Select( each => (each ?? "").ToString()).JoinWithComma(), powershell.InvocationStateInfo.Reason)); +#endif + + var input = new PSDataCollection(); + input.Complete(); + + var output = new PSDataCollection(); + + if (outputAction != null) + { + output.DataAdded += outputAction; + } + + if (errorAction != null) + { + powershell.Streams.Error.DataAdded += errorAction; + } + + powershell.Invoke(null, output, new PSInvocationSettings()); + + if (output.Count == 0) + { + return default(T); + } + + // return the last output to the user + PSObject last = output.Last(); + + if (last != null) + { + // convert last to T + return (T)last.ImmediateBaseObject; + } + } + + return default(T); + } + + internal static PowerShell WaitForReady(this PowerShell powershell) { + try { + if (powershell != null) { + switch (powershell.InvocationStateInfo.State) { + case PSInvocationState.Stopping: + while (powershell.InvocationStateInfo.State == PSInvocationState.Stopping) { + Thread.Sleep(10); + } + break; + + case PSInvocationState.Running: + powershell.Stop(); + while (powershell.InvocationStateInfo.State == PSInvocationState.Stopping) { + Thread.Sleep(10); + } + break; + + case PSInvocationState.Failed: + break; + case PSInvocationState.Completed: + break; + + case PSInvocationState.Stopped: + break; + + case PSInvocationState.NotStarted: + break; + case PSInvocationState.Disconnected: + break; + } + } + } + catch (Exception e) { + e.Dump(); + } + return powershell; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/PowerShellMetaProvider.cs b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/PowerShellMetaProvider.cs new file mode 100644 index 000000000..56b2d98b0 --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/PowerShellMetaProvider.cs @@ -0,0 +1,830 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.MetaProvider.PowerShell.Internal { + using System; + using System.Collections; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Management.Automation; + using System.Reflection; + using System.Threading.Tasks; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Internal.Utility.Versions; + using Microsoft.PackageManagement.Internal.Utility.Collections; + using System.Globalization; + using Implementation; + using PackageManagement.Internal; + using PackageManagement.Internal.Implementation; + using PackageManagement.Internal.Providers; + using PackageManagement.Internal.Utility.Plugin; + using ErrorCategory = System.Management.Automation.ErrorCategory; + using ProviderOption = PackageManagement.Internal.Implementation.PackageManagementService.ProviderOption; + + internal class ProviderItem { + internal PSModuleInfo ModuleInfo; + internal string ProviderPath; + } + + /// + /// A MetaProvider class that loads Providers implemented as a PowerShell Module. + /// It connects the functions in the PowerShell module to the expected functions that the + /// interface expects. + /// + public class PowerShellMetaProvider : IDisposable { + private static readonly HashSet _exclusionList = new HashSet { + "AppBackgroundTask", + "AppLocker", + "Appx", + "AssignedAccess", + "BitLocker", + "BitsTransfer", + "BranchCache", + "CimCmdlets", + "Defender", + "DirectAccessClientComponents", + "Dism", + "DnsClient", + "Hyper-V", + "International", + "iSCSI", + "ISE", + "Kds", + "Microsoft.PowerShell.Diagnostics", + "Microsoft.PowerShell.Host", + "Microsoft.PowerShell.Management", + "Microsoft.PowerShell.Security", + "Microsoft.PowerShell.Utility", + "Microsoft.WSMan.Management", + "MMAgent", + "MsDtc", + "NetAdapter", + "NetConnection", + "NetEventPacketCapture", + "NetLbfo", + "NetNat", + "NetQos", + "NetSecurity", + "NetSwitchTeam", + "NetTCPIP", + "NetWNV", + "NetworkConnectivityStatus", + "NetworkTransition", + "PcsvDevice", + "PKI", + "PrintManagement", + "PSDiagnostics", + "PSScheduledJob", + "PSWorkflow", + "PSWorkflowUtility", + "ScheduledTasks", + "SecureBoot", + "SmbShare", + "SmbWitness", + "StartScreen", + "Storage", + "TLS", + "TroubleshootingPack", + "TrustedPlatformModule", + "VpnClient", + "Wdac", + "WindowsDeveloperLicense", + "WindowsErrorReporting", + "WindowsSearch", + "PackageManagement", // dont' search ourselves. + "OneGet", // dont' search ourselves. + "OneGet-Edge" // dont' search ourselves. + }; + + private static string _baseFolder; + private static string _powershellProviderFunctionsPath; + + //The reason of using 'object' instead of' PowerShellPackageProvider' is that PowerShellPackageProvider is a provider + //that is not visiable to the PackageManagment. + private readonly IDictionary _availableProviders = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly IDictionary> _psProviderCacheTable = new Dictionary>(StringComparer.OrdinalIgnoreCase); + internal const string PowerShellGet = "PowerShellGet"; + private static readonly PowerShell _powershell = PowerShell.Create(); + private static bool _initialized = false; + + public PowerShellMetaProvider() { + // _packageProviders.BlockingEnumerator = true; + } + + internal static string BaseFolder { + get { + if (_baseFolder == null) { +#if CORECLR + _baseFolder = Path.GetDirectoryName(Path.GetFullPath(typeof(PowerShellMetaProvider).GetTypeInfo().Assembly.ManifestModule.FullyQualifiedName)); +#else + _baseFolder = Path.GetDirectoryName(Path.GetFullPath(Assembly.GetExecutingAssembly().Location)); +#endif + if (_baseFolder == null || !Directory.Exists(_baseFolder)) { + throw new Exception(Resources.Messages.CantFindBasePowerShellModuleFolder); + } + } + return _baseFolder; + } + } + + static PowerShellMetaProvider() { + try { + EnsurePowerShellInitialized(); + } catch { + //we capture the exceptions here so that we won't fail to load the metaprovider assembly + //exceptions can happen when execution-policy is set to restrict, for example. + } + } + + static void EnsurePowerShellInitialized() { + if (!_initialized) { + _powershell.ImportModule(PowerShellProviderFunctions); + _initialized = true; + } + } + + internal static string PowerShellProviderFunctions { + get { + if (_powershellProviderFunctionsPath == null) { + // try the etc directory + _powershellProviderFunctionsPath = Path.Combine(BaseFolder, "etc", "PackageProviderFunctions.psm1"); + if (!File.Exists(_powershellProviderFunctionsPath)) { + // fall back to the same directory. + _powershellProviderFunctionsPath = Path.Combine(BaseFolder, "PackageProviderFunctions.psm1"); + if (!File.Exists(_powershellProviderFunctionsPath)) { + // oh-oh, no powershell functions file. + throw new Exception(String.Format(CultureInfo.CurrentCulture, Resources.Messages.UnableToFindPowerShellFunctionsFile, _powershellProviderFunctionsPath)); + } + } + } + return _powershellProviderFunctionsPath; + } + } + + public IEnumerable ProviderNames { + get { + //return _packageProviders.Select(each => each.GetPackageProviderName());; + return _availableProviders.Keys; + } + } + + private void AddToTable(string name, ProviderItem provider) { + //try to find if the provider is in the table already + + if (_psProviderCacheTable.ContainsKey(name)) { + var list = _psProviderCacheTable[name]; + + var index = list.FindIndex(each => (each.ModuleInfo.Version == provider.ModuleInfo.Version) && (each.ProviderPath.EqualsIgnoreCase(provider.ProviderPath))); + + if (index != -1) { + list[index] = provider; + + } else { + _psProviderCacheTable[name].Add(provider); + } + } else { + var entry = new List { + provider + }; + _psProviderCacheTable.Add(name, entry); + } + } + + /// + /// The name of this MetaProvider class + /// + public string MetaProviderName { + get { + return "PowerShell"; + } + } + + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + public string GetProviderPath(string providername) { + return _availableProviders.Keys.Where(each => each.EqualsIgnoreCase(providername)) + .Select(each => ((PowerShellPackageProvider)_availableProviders[each]).ModulePath).FirstOrDefault(); + } + + private IEnumerable> ScanPrivateDataForProviders(PsRequest request, string baseFolder, Hashtable privateData, PSModuleInfo moduleInfo) { + var providers = privateData.GetStringCollection("PackageManagementProviders").ReEnumerable(); + if (providers.Any()) { + // found a module that is advertizing one or more Providers. + + foreach (var provider in providers) { + var fullPath = provider; + try { + if (!Path.IsPathRooted(provider)) { + fullPath = Path.GetFullPath(Path.Combine(baseFolder, provider)); + } + } catch { + // got an error from the path. + continue; + } + if (Directory.Exists(fullPath) || File.Exists(fullPath)) { + // looks like we have something that could definitely be a + // a module path. + var result = new KeyValuePair(fullPath, moduleInfo); + AddToPowerShellProviderCacheTable(result); + yield return result; + } else { + request.Verbose(string.Format(CultureInfo.CurrentCulture, Resources.Messages.FileNotFound, fullPath)); + } + } + } else { + request.Debug(string.Format(Resources.Messages.PackageManagementProvidersNotFound, baseFolder)); + } + } + + private IEnumerable> GetPackageManagementModules(PsRequest request, PSModuleInfo module, Version requiredVersion, Version minimumVersion, Version maximumVersion) { + // skip modules that we know don't contain any PM modules + if (!_exclusionList.Contains(module.Name)) { + var privateData = module.PrivateData as Hashtable; + if (privateData != null) { + if (requiredVersion != null) { + if ((FourPartVersion)module.Version == (FourPartVersion)requiredVersion) { + return ScanPrivateDataForProviders(request, Path.GetDirectoryName(module.Path), privateData, module).ToArray(); + } else { + return Enumerable.Empty>(); + } + } + + if ((minimumVersion != null) && ((FourPartVersion)module.Version < (FourPartVersion)minimumVersion)) { + return Enumerable.Empty>(); + } + + if ((maximumVersion != null) && ((FourPartVersion)module.Version > (FourPartVersion)maximumVersion)) { + return Enumerable.Empty>(); + } + + return ScanPrivateDataForProviders(request, Path.GetDirectoryName(module.Path), privateData, module).ToArray(); + } + } + return Enumerable.Empty>(); + } + + private void AddToPowerShellProviderCacheTable(KeyValuePair moduleInfo) { + + //some times when PrivateData in a provider's .psd1 meta file contains multiple providers (not recommanded way), + //they all reside under the same module path. So we extract each file name and add to the table as dictionary key + //to indicate they are actaully different providers. + if (moduleInfo.Key != null) { + var name = Path.GetFileNameWithoutExtension(moduleInfo.Key); + if (string.IsNullOrWhiteSpace(name)) { + return; + } + + // for the case where provider is a dll, we enforce that provider name must be the same as module name + // so the name we add to the hash table will be the name of the module. + // (for the case where provider is not a dll, we still allow multiple provider names for a module) + if (string.Equals(Path.GetExtension(moduleInfo.Key), ".dll", StringComparison.OrdinalIgnoreCase)) + { + name = moduleInfo.Value.Name; + } + + AddToTable(name, + new ProviderItem { + ModuleInfo = moduleInfo.Value, + ProviderPath = moduleInfo.Key, + }); + } + } + + private static PackageManagementService PackageManagementService { + get { + return PackageManager.Instance as PackageManagementService; + } + } + + //key = path, value = PSModuleInfo + private IEnumerable> ScanForModules( + PsRequest request, + Version requiredVersion, + Version minimumVersion, + Version maximumVersion, + ProviderOption providerOption = ProviderOption.LatestVersion) { + + // two places we search for modules + // 1. in this assembly's folder, look for all psd1 and psm1 files. + // + // 2. modules in the PSMODULEPATH + // + // Import each one of those, and check to see if they have a PackageManagementProviders section in their private data + + var allAvailableModules = _powershell + .Clear() + .AddCommand("Get-Module") + .AddParameter("ListAvailable") + .Invoke(); + + return allAvailableModules.WhereNotNull() + .SelectMany(each => GetPackageManagementModules(request, each, requiredVersion, minimumVersion, maximumVersion)); + } + + private IEnumerable> ScanForPowerShellGetModule(PsRequest request) + { + var psget = _powershell.GetModule("PowerShellGet").FirstOrDefault(); + return psget != null ? GetPackageManagementModules(request, psget, null, null, null) : Enumerable.Empty>(); + } + + private IEnumerable AlternativeModuleScan( + PsRequest request, + Version requiredVersion, + Version minimumVersion, + Version maximumVersion, + ProviderOption providerOption = ProviderOption.LatestVersion) { + + var psModulePath = Environment.GetEnvironmentVariable("PSModulePath") ?? ""; + + IEnumerable paths = psModulePath.Split(new char[] {';'}, StringSplitOptions.RemoveEmptyEntries); + + // add assumed paths just in case the environment variable isn't really set. + try { +#if CORECLR + paths = paths.ConcatSingleItem(Path.Combine(Environment.GetEnvironmentVariable("windir"), "system32", @"WindowsPowerShell\v1.0\Modules")); +#else + paths = paths.ConcatSingleItem(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), @"WindowsPowerShell\v1.0\Modules")); +#endif + } catch { + // skip the folder if it's not valid + } + try { +#if CORECLR + paths = paths.ConcatSingleItem(Path.Combine(Environment.GetEnvironmentVariable("userprofile"), "documents", @"WindowsPowerShell\Modules")); +#else + paths = paths.ConcatSingleItem(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), @"WindowsPowerShell\Modules")); +#endif + } catch { + // skip the folder if it's not valid + } + + if (!string.IsNullOrWhiteSpace(BaseFolder) && BaseFolder.DirectoryExists()) { + paths = paths.ConcatSingleItem(BaseFolder); + } + + var moduleFolders = paths.Distinct(new PathEqualityComparer(PathCompareOption.Full)).Where(each => each.DirectoryExists()).SelectMany(each => Directory.EnumerateDirectories(each).Where(dir => !_exclusionList.Contains(Path.GetFileName(dir)))); + + foreach (var module in moduleFolders) { + + var moduleManifest = Path.Combine(module, Path.GetFileName(module) + ".psd1"); + if (File.Exists(moduleManifest)) { + //The version check is defered in the GetPackageManagementModules() because we do not know the version without looking into the content. + yield return moduleManifest; + } + + //The following are the cases where there are multiple modules installed on the system. The file folder is the module version. + var selectedVersions = Directory.EnumerateDirectories(module).Select(dir => new { + folder = dir, + ver = (FourPartVersion)Path.GetFileName(dir) + }).Where(each => each.ver > 0L); + + if (requiredVersion != null) { + var version = selectedVersions.Where(each => each.ver == (FourPartVersion)requiredVersion).Select(each => each.folder).FirstOrDefault(); + if (version != null) { + var file = Path.Combine(version, Path.GetFileName(Path.GetFileName(module)) + ".psd1"); + if (File.Exists(file)) { + yield return file; + } + } + } + + if (minimumVersion != null) { + selectedVersions = selectedVersions.Where(each => each.ver >= (FourPartVersion)minimumVersion); + + } + if (maximumVersion != null) { + selectedVersions = selectedVersions.Where(each => each.ver <= (FourPartVersion)maximumVersion); + } + + var results = (providerOption == PackageManagementService.ProviderOption.AllProvider) ? + selectedVersions.Select(version => Path.Combine(version.folder, Path.GetFileName(Path.GetFileName(module)) + ".psd1")).Where(File.Exists) : + new[] { + selectedVersions.OrderByDescending(each => each.ver) + .Select(version => Path.Combine(version.folder, Path.GetFileName(Path.GetFileName(module)) + ".psd1")) + .FirstOrDefault(File.Exists) + }; + + foreach (var result in results.WhereNotNull()) { + yield return result; + + } + } + } + + public object CreateProvider(string name) { + + if (_availableProviders.ContainsKey(name)) { + var provider = _availableProviders[name]; + + if (provider != null) { + return provider; + } + } + + // it's possible that this is a path passed in. Let's see if it's a provider. + if (!string.IsNullOrEmpty(name) && name.FileExists()) { + // MUST DO: load provider from filepath. + } + // create the instance + throw new Exception("No provider by name '{0}' registered.".format(name)); + } + + protected virtual void Dispose(bool disposing) { + if (disposing) { + _availableProviders.Clear(); + _psProviderCacheTable.Clear(); + } + } + + private PowerShellPackageProvider Create(PsRequest request, string modulePath, string requiredVersion, bool force, bool logWarning) { + var ps = PowerShell.Create(); + try { + // load the powershell provider functions into this runspace. + if (ps.ImportModule(PowerShellProviderFunctions, false) != null) { + var result = ps.ImportModule(modulePath, force); + if (result != null) { + try { + return new PowerShellPackageProvider(ps, result, requiredVersion); + } catch (Exception e) { + e.Dump(); + } + } + } + } catch (Exception e) { + // something didn't go well. + // skip it. + e.Dump(); + if (logWarning) { + request.Warning(e.Message); + } + } + + // didn't import correctly. + ps.Dispose(); + return null; + } + + public void InitializeProvider(PsRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + request.Debug("Initializing PowerShell MetaProvider"); + + //During the initialization, we load the PowerShellGet only to speeding up a little bit + var psModules = ScanForPowerShellGetModule(request).WhereNotNull(); + + foreach (var psModule in psModules) { + //Check if the PowerShellGet provider exists + if ((psModule.Key != null) && (psModule.Value != null)) { + //load the PowerShellGet + AnalyzeModule(request, psModule.Key, psModule.Value.Version ?? new Version(0, 0), false, true, psModule.Value); + } + } + + if (_availableProviders.ContainsKey(PowerShellGet)) + { + request.Debug("Loaded PowerShell Provider: PowerShellGet"); + } + else + { + //if we can not load PowerShellGet, we do not fail the initialization + request.Verbose(string.Format(CultureInfo.CurrentCulture, Resources.Messages.ModuleNotFound, PowerShellGet)); + } + } + + + private void AddToPackageProviderCacheTable(PackageProvider provider) + { + PackageManagementService.AddToProviderCacheTable(provider.ProviderName, provider); + } + + + public void RefreshProviders(PsRequest request, string providerName, Version requiredVersion, Version minimumVersion, Version maximumVersion, bool logWarning) { + //find and load the latest versions of the providers if only providerName exists, e.g., get-pp -name or import-pp -name + //find and load the particular provider if both providerName and version are provided + _psProviderCacheTable.Clear(); + + EnsurePowerShellInitialized(); + + if (!string.IsNullOrWhiteSpace(providerName)) { + //Get the list of available providers + var modules = ScanForModules(request, requiredVersion, minimumVersion, maximumVersion, ProviderOption.AllProvider).ReEnumerable(); + + var tasks = modules.AsyncForEach(modulePath => AnalyzeModule(request, modulePath.Key, modulePath.Value.Version ?? new Version(0, 0), false, logWarning, modulePath.Value)); + tasks.WaitAll(); + } else { + //find all providers but only load the lastest if no name nor version exists, e.g. get-pp -list + + //Scan for the all available providers + var results = ScanForModules(request, null, null, null, ProviderOption.AllProvider).ToArray(); + + if (!_psProviderCacheTable.Any()) { + return; + } + + foreach (var list in _psProviderCacheTable.Values.WhereNotNull()) { + var psInfo = list.OrderByDescending(each => each.ModuleInfo.Version).ToArray(); + if (!psInfo.Any()) { + continue; + } + + PackageProvider pkgProvider = null; + for (var index = 0; index < psInfo.Length; index++) { + var providerItem = psInfo[index]; + if (providerItem == null) { + continue; + } + + // if the provider is a dll, we will just provide a default provider, assuming the module name will be the name of the provider and add to the cache table (if it is not already loaded) + if (string.Equals(Path.GetExtension(providerItem.ProviderPath), ".dll", StringComparison.OrdinalIgnoreCase)) + { + // check whether it is already loaded or not + var loadedProvider = PackageManagementService.PackageProviders.FirstOrDefault(item => string.Equals(item.ProviderPath, providerItem.ProviderPath, StringComparison.OrdinalIgnoreCase)); + + // only provide default provider if it is not loaded + if (loadedProvider == null) + { + // here moduleinfo.providercategory is the module name. + // we are applying the guideline that module name is the same as provider name so we use that as the provider name + AddToPackageProviderCacheTable(new PackageProvider(new DefaultPackageProvider(providerItem.ModuleInfo.Name, providerItem.ModuleInfo.Version.ToString())) + { + ProviderPath = providerItem.ProviderPath, + Version = providerItem.ModuleInfo.Version, + IsLoaded = false + }); + } + + continue; + } + + //load the provider that has the latest version + if (pkgProvider == null) { + // analyze the module + pkgProvider = AnalyzeModule(request, providerItem.ProviderPath, providerItem.ModuleInfo.Version, false, logWarning, providerItem.ModuleInfo); + } else { + //the rest of providers under the same module will just create a provider object for the output but not loaded + var packageProvider = new PackageProvider(new DefaultPackageProvider(pkgProvider.ProviderName, providerItem.ModuleInfo.Version.ToString())) + { + ProviderPath = providerItem.ProviderPath, + Version = providerItem.ModuleInfo.Version, + IsLoaded = false + }; + AddToPackageProviderCacheTable(packageProvider); + } + } + } + + _psProviderCacheTable.Clear(); + } + } + + private IEnumerable FindMatchedProvidersFromInternalCacheTable(PsRequest request, string providerPath) { + //Search from the internal table to see if we can the matched provider + + var providers = PackageManagementService.ProviderCacheTable + .Select(each => each.Value).Select(list => list + .Where(each => each.ProviderPath.EqualsIgnoreCase(providerPath) && each.IsLoaded) + .Select(each => each)); + + return providers.SelectMany(list => { + var packageProviders = list as PackageProvider[] ?? list.ToArray(); + return packageProviders; + }); + } + + public IEnumerable LoadAvailableProvider(PsRequest request, string modulePath, Version requiredVersion, bool force) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + if (string.IsNullOrEmpty(modulePath)) { + throw new ArgumentNullException("modulePath"); + } + + EnsurePowerShellInitialized(); + + //Check if it is already in the cache table + var providersAlreadyImported = FindMatchedProvidersFromInternalCacheTable(request, modulePath).ToArray(); + + if (providersAlreadyImported.Any() && !force) { + return providersAlreadyImported; + } + + //Trying to load it from the path directly + var pkgProvider = AnalyzeModule(request, modulePath, requiredVersion ?? new Version(0, 0), force); + if (pkgProvider != null) { + return new[] { pkgProvider }.WhereNotNull(); + } + + request.Error(PackageManagement.Internal.Constants.Messages.FailedToImportProvider, + ErrorCategory.InvalidOperation.ToString(), PowerShellGet, string.Format(CultureInfo.CurrentCulture, Resources.Messages.FailedToImportProvider, modulePath)); + return Enumerable.Empty(); + } + + private PackageProvider AnalyzeModule(PsRequest request, string modulePath, Version requiredVersion, bool force, bool logWarning =true, PSModuleInfo psModuleInfo = null) + { + if (string.IsNullOrWhiteSpace(modulePath)) { + return null; + } + + request.Debug("Attempting to load PowerShell Provider Module [{0}]", modulePath); + + if (string.Equals(Path.GetExtension(modulePath), ".dll", StringComparison.OrdinalIgnoreCase)) + { + if (psModuleInfo != null) + { + // fake provider and returns it + var result = new PackageProvider(new DefaultPackageProvider(psModuleInfo.Name, psModuleInfo.Version.ToString())) + { + ProviderPath = modulePath, + Version = psModuleInfo.Version, + IsLoaded = false + }; + + AddToPackageProviderCacheTable(result); + + return result; + } + else + { + // psmoduleinfo is only null when this function is called in loadavailableprovider + // but in that case we want to load the provider directly anyway so we can do this + // if the path is a .dll then we ask packagemanagement to load it for us + // it will also register the dll + PackageManagementService.LoadProviderAssembly(request, modulePath, true); + + // now let's checked whether we can find it in the list of loaded providers + foreach (var loadedProvider in PackageManagementService.PackageProviders) + { + // the one loaded should have the same path + if (string.Equals(loadedProvider.ProviderPath, modulePath, StringComparison.OrdinalIgnoreCase)) + { + return loadedProvider; + } + } + + // if we reached here then we have failed to load the provider :( + return null; + } + } + + string requiredVersionString = requiredVersion.ToString(); + var provider = Create(request, modulePath, requiredVersionString, force, logWarning); + if (provider != null) { + var providerName = provider.GetPackageProviderName(); + if (!string.IsNullOrWhiteSpace(providerName)) { + request.Debug(string.Format(CultureInfo.CurrentCulture, Resources.Messages.SuccessfullyLoadedModule, modulePath)); + // looks good to me, let's add this to the list of moduels this meta provider can create. + + var packageProvider = new PackageProvider(provider.As()) { + IsLoaded = true, + Version = provider.GetProviderVersion(), + ProviderPath = modulePath + }; + + // take out powershell get + var psgetprovider = PackageManagementService.PackageProviders.FirstOrDefault(pv => string.Equals(pv.ProviderName, PowerShellGet, StringComparison.OrdinalIgnoreCase)); + + if (psModuleInfo != null) + { + // add swidtag information using moduleinfo + // however, this won't give us as much information yet + // we may have to fill this up later + ProvideSwidTagInformation(packageProvider, psModuleInfo); + } + + AddToPackageProviderCacheTable(packageProvider); + _availableProviders.AddOrSet(providerName, provider); + + return packageProvider; + } else { + provider.Dispose(); + provider = null; + request.Debug(string.Format(CultureInfo.CurrentCulture, Resources.Messages.ProviderNameIsNullOrEmpty, modulePath)); + } + } + return null; + } + + /// + /// If we cannot use psget to get swidtag information, we will try to fill in some of the information + /// + /// + /// + private void ProvideSwidTagInformation(PackageProvider packageProvider, PSModuleInfo psModuleInfo) + { + if (packageProvider == null || psModuleInfo == null) + { + return; + } + + packageProvider.VersionScheme = "MultiPartNumeric"; + + Microsoft.PackageManagement.Internal.Packaging.SoftwareMetadata softwareMetadata = new Microsoft.PackageManagement.Internal.Packaging.SoftwareMetadata(); + bool changed = false; + + var type = psModuleInfo.GetType(); + + // introduced in ps 2.0 + if (!string.IsNullOrWhiteSpace(psModuleInfo.Description)) + { + softwareMetadata.Description = psModuleInfo.Description; + changed = true; + } + + // introduced in ps 3.0 + if (!string.IsNullOrWhiteSpace(psModuleInfo.Copyright)) + { + softwareMetadata.AddAttribute("copyright", psModuleInfo.Copyright); + changed = true; + } + + // tags is introduced in ps 5.0 + var tagsProperty = type.GetProperty("Tags"); + bool isV5 = tagsProperty != null; + + if (isV5) + { + // introduced in ps 5.0 + var tags = tagsProperty.GetValue(psModuleInfo); + + // check that we have something in tags + if (tags is IEnumerable && (tags as IEnumerable).Any()) + { + softwareMetadata.AddAttribute("tags", string.Join(" ", (tags as IEnumerable).Distinct())); + changed = true; + } + + var releaseNotes = type.GetProperty("ReleaseNotes").GetValue(psModuleInfo); + + // check that we have something in releasenotes + if (releaseNotes is string && !string.IsNullOrWhiteSpace(type.GetProperty("ReleaseNotes").GetValue(psModuleInfo) as string)) + { + softwareMetadata.AddAttribute("tags", string.Join(" ", (tags as IEnumerable).Distinct())); + changed = true; + } + } + + if (changed) + { + packageProvider.AddElement(softwareMetadata); + } + + if (isV5) + { + var iconUri = type.GetProperty("IconUri").GetValue(psModuleInfo); + + // introduced in ps 5.0 + if (iconUri is Uri) + { + packageProvider.AddLink(iconUri as Uri, "icon"); + } + + var licenseUri = type.GetProperty("LicenseUri").GetValue(psModuleInfo); + + // introduced in ps 5.0 + if (licenseUri is Uri) + { + packageProvider.AddLink(licenseUri as Uri, "license"); + } + + var projectUri = type.GetProperty("ProjectUri").GetValue(psModuleInfo); + + // introduced in ps 5.0 + if (projectUri is Uri) + { + packageProvider.AddLink(projectUri as Uri, "project"); + } + + } + + // introduced in ps 3.0 + if (!string.IsNullOrWhiteSpace(psModuleInfo.Author)) + { + packageProvider.AddEntity(psModuleInfo.Author, null, "author"); + } + + // introduced in ps 3.0 + if (!string.IsNullOrWhiteSpace(psModuleInfo.CompanyName)) + { + packageProvider.AddEntity(psModuleInfo.CompanyName, null, "owner"); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/PowerShellPackageProvider.cs b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/PowerShellPackageProvider.cs new file mode 100644 index 000000000..daff94fdd --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/PowerShellPackageProvider.cs @@ -0,0 +1,290 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.MetaProvider.PowerShell.Internal { + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Management.Automation; + + using Packaging; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Internal.Utility.Versions; + using Microsoft.PackageManagement.Internal.Api; + using Microsoft.PackageManagement.Internal.Utility.Collections; + + public class PowerShellPackageProvider : PowerShellProviderBase { + private static int _findId = 1; + private readonly Lazy>> _findByNameBatches = new Lazy>>(() => new Dictionary>()); + private readonly Lazy>> _findByFileBatches = new Lazy>>(() => new Dictionary>()); + private readonly Lazy>> _findByUriBatches = new Lazy>>(() => new Dictionary>()); + private string _version; + + public PowerShellPackageProvider(PowerShell ps, PSModuleInfo module, string version) : base(ps, module) { + _version = version; + } + + private bool IsFirstParameterType(string function) { + var method = GetMethod(function); + if (method == null) { + return false; + } + + return method.Parameters.Values.First().ParameterType == typeof (T); + } + + public bool IsMethodImplemented(string methodName) { + if (methodName == null) { + throw new ArgumentNullException("methodName"); + } + + if (methodName.EqualsIgnoreCase("startfind") || methodName.EqualsIgnoreCase("completeFind") || methodName.EqualsIgnoreCase("GetProviderVersion")) { + return true; + } +#if DEEP_DEBUG + var r = GetMethod(methodName) != null; + if (!r) { + Debug.WriteLine(" -> '{0}' Not Found In PowerShell Module '{1}'".format(methodName, _module.Name)); + } + return r; +#else + return GetMethod(methodName) != null; +#endif + } + + private object Call(string function, IRequest requestObject, params object[] args) { + return PsRequest.New(requestObject, this, function).CallPowerShell(args); + } + + #region implement PackageProvider-interface + + public void AddPackageSource(string name, string location, bool trusted, IRequest requestObject) { + Call("AddPackageSource", requestObject, name, location, trusted); + } + public void FindPackage(string name, string requiredVersion, string minimumVersion, string maximumVersion, int id, IRequest requestObject) { + // special case. + // if FindPackage is implemented taking an array of strings + // and the id > 0 then we need to hold onto the collection until + // CompleteFind is called. + + // if it expects multiples... + if (IsFirstParameterType("FindPackage")) { + if (id > 0) { + _findByNameBatches.Value.GetOrAdd(id, () => new List()).Add(name, requiredVersion, minimumVersion, maximumVersion); + return; + } + + // not passed in as a set. + Call("FindPackage", requestObject, new string[] { + name + }, requiredVersion, minimumVersion, maximumVersion); + return; + } + + // otherwise, it has to take them one at a time and yield them anyway. + Call("FindPackage",requestObject, name, requiredVersion, minimumVersion, maximumVersion); + } + public void FindPackageByFile(string file, int id, IRequest requestObject) { + // special case. + // if FindPackageByFile is implemented taking an array of strings + // and the id > 0 then we need to hold onto the collection until + // CompleteFind is called. + + // if it expects multiples... + if (IsFirstParameterType("FindPackageByFile")) { + if (id > 0) { + _findByFileBatches.Value.GetOrAdd(id, () => new List()).Add(file); + return; + } + // not passed in as a set. + Call("FindPackageByFile",requestObject, new string[] { + file + }); + return; + } + + Call("FindPackageByFile", requestObject, file); + return; + } + public void FindPackageByUri(Uri uri, int id, IRequest requestObject) { + // special case. + // if FindPackageByUri is implemented taking an array of strings + // and the id > 0 then we need to hold onto the collection until + // CompleteFind is called. + + // if it expects multiples... + if (IsFirstParameterType("FindPackageByUri")) { + if (id > 0) { + _findByUriBatches.Value.GetOrAdd(id, () => new List()).Add(uri); + return; + } + // not passed in as a set. + Call("FindPackageByUri",requestObject, new Uri[] { + uri + }); + return; + } + + Call("FindPackageByUri",requestObject, uri); + } + + /// + /// Returns the packages that are installed + /// + /// the package name to match. Empty or null means match everything + /// the specific version asked for. If this parameter is specified (ie, not null or empty string) then the minimum and maximum values are ignored + /// the minimum version of packages to return . If the requiredVersion parameter is specified (ie, not null or empty string) this should be ignored + /// the maximum version of packages to return . If the requiredVersion parameter is specified (ie, not null or empty string) this should be ignored + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void GetInstalledPackages(string name, string requiredVersion, string minimumVersion, string maximumVersion, IRequest requestObject) { + Call("GetInstalledPackages",requestObject, name,requiredVersion,minimumVersion,maximumVersion); + } + + public void GetDynamicOptions(string category, IRequest requestObject) { + OptionCategory cat; + if (Enum.TryParse(category ?? "", true, out cat)) { + // if this version of the plugin doesn't support that category + // there's no point in trying to get options from that type. + Call("GetDynamicOptions", requestObject, cat); + } + } + + private string _providerName; + /// + /// Returns the name of the Provider. + /// + /// + /// the name of the package provider + public string GetPackageProviderName() { + return _providerName ?? (_providerName = (string)CallPowerShellWithoutRequest("GetPackageProviderName")); + } + public void ResolvePackageSources(IRequest requestObject) { + Call("ResolvePackageSources", requestObject); + } + + public void InitializeProvider(IRequest requestObject) { + Call("InitializeProvider", requestObject); + } + + public string GetProviderVersion() { + var result= (string)CallPowerShellWithoutRequest("GetProviderVersion"); + + if (string.IsNullOrWhiteSpace(result)) { + + if (!string.IsNullOrEmpty(_version)) { + return _version; + } + + if (_module.Version != new Version(0, 0, 0, 0)) { + result = _module.Version.ToString(); + } else { + try +{ + // use the latest date as a version number + return (FourPartVersion) _module.FileList.Max(each => new FileInfo(each).LastWriteTime); + } catch { + // I give up. + return "0.0.0.1"; + } + } + } + return result; + } + public void InstallPackage(string fastPath, IRequest requestObject) { + Call("InstallPackage", requestObject, fastPath); + } + public void RemovePackageSource(string name, IRequest requestObject) { + Call("RemovePackageSource", requestObject, name); + } + public void UninstallPackage(string fastPath, IRequest requestObject) { + Call("UninstallPackage", requestObject, fastPath); + } + public void GetFeatures(IRequest requestObject) { + Call("GetFeatures", requestObject); + } + + // --- operations on a package --------------------------------------------------------------------------------------------------- + public void DownloadPackage(string fastPath, string location, IRequest requestObject) { + Call("DownloadPackage", requestObject, fastPath, location); + } + + public void GetPackageDetails(string fastPath, IRequest requestObject) { + Call("GetPackageDetails", requestObject, fastPath); + } + public int StartFind(IRequest requestObject) { + lock (this) { + return ++_findId; + } + } + public void CompleteFind(int id, IRequest requestObject) { + if (id < 1) { + return; + } + + if (_findByNameBatches.IsValueCreated) { + var nameBatch = _findByNameBatches.Value.TryPullValue(id); + if (nameBatch != null) { + if (IsFirstParameterType("FindPackage")) { + // it takes a batch at a time. + + var names = nameBatch.Select(each => each.Item1).ToArray(); + var p1 = nameBatch[0]; + + Call("FindPackage", requestObject,names, p1.Item2, p1.Item3, p1.Item4); + } else { + foreach (var each in nameBatch) { + Call("FindPackage",requestObject, each.Item1, each.Item2, each.Item3, each.Item4); + } + } + } + } + + if (_findByFileBatches.IsValueCreated) { + var fileBatch = _findByFileBatches.Value.TryPullValue(id); + if (fileBatch != null) { + if (IsFirstParameterType("FindPackageByFile")) { + // it takes a batch at a time. + Call("FindPackageByFile", requestObject, new object[] { + fileBatch.ToArray() + }); + } else { + foreach (var each in fileBatch) { + Call("FindPackageByFile",requestObject, each); + } + } + } + } + + if (_findByUriBatches.IsValueCreated) { + var uriBatch = _findByUriBatches.Value.TryPullValue(id); + if (uriBatch != null) { + if (IsFirstParameterType("FindPackageByUri")) { + // it takes a batch at a time. + Call("FindPackageByUri",requestObject, new object[] {uriBatch.ToArray()}); + } else { + foreach (var each in uriBatch) { + Call("FindPackageByUri",requestObject, each); + } + } + } + } + } + #endregion + + } +} diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/PowerShellProviderBase.cs b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/PowerShellProviderBase.cs new file mode 100644 index 000000000..425fb42ea --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/PowerShellProviderBase.cs @@ -0,0 +1,293 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Microsoft.PackageManagement.Internal.Utility.Platform; + +namespace Microsoft.PackageManagement.MetaProvider.PowerShell.Internal { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Management.Automation; + using System.Threading; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Internal.Utility.Platform; + using Messages = Microsoft.PackageManagement.MetaProvider.PowerShell.Resources.Messages; + using System.Collections.Concurrent; + + public class PowerShellProviderBase : IDisposable { + private object _lock = new Object(); + protected PSModuleInfo _module; + private PowerShell _powershell; + private ManualResetEvent _reentrancyLock = new ManualResetEvent(true); + private readonly Dictionary _allCommands = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _methods = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public PowerShellProviderBase(PowerShell ps, PSModuleInfo module) { + if (module == null) { + throw new ArgumentNullException("module"); + } + + _powershell = ps; + _module = module; + + // combine all the cmdinfos we care about + // but normalize the keys as we go (remove any '-' '_' chars) + foreach (var k in _module.ExportedAliases.Keys) { + _allCommands.AddOrSet(k.Replace("-", "").Replace("_", ""), _module.ExportedAliases[k]); + } + foreach (var k in _module.ExportedCmdlets.Keys) { + _allCommands.AddOrSet(k.Replace("-", "").Replace("_", ""), _module.ExportedCmdlets[k]); + } + foreach (var k in _module.ExportedFunctions.Keys) { + _allCommands.AddOrSet(k.Replace("-", "").Replace("_", ""), _module.ExportedFunctions[k]); + } + } + + public string ModulePath { + get { + return _module.Path; + } + } + + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) { + if (disposing) { + if (_powershell != null) { + _powershell.Dispose(); + _powershell = null; + } + + if (_reentrancyLock != null) { + _reentrancyLock.Dispose(); + _reentrancyLock = null; + } + + _module = null; + } + } + + internal CommandInfo GetMethod(string methodName) { + return _methods.GetOrAdd(methodName, () => { + if (_allCommands.ContainsKey(methodName)) { + return _allCommands[methodName]; + } + + // try simple plurals to single + if (methodName.EndsWith("s", StringComparison.OrdinalIgnoreCase)) { + var meth = methodName.Substring(0, methodName.Length - 1); + if (_allCommands.ContainsKey(meth)) { + return _allCommands[meth]; + } + } + + // try words like Dependencies to Dependency + if (methodName.EndsWith("cies", StringComparison.OrdinalIgnoreCase)) { + var meth = methodName.Substring(0, methodName.Length - 4) + "cy"; + if (_allCommands.ContainsKey(meth)) { + return _allCommands[meth]; + } + } + + // try IsFoo to Test-IsFoo + if (methodName.IndexOf("Is", StringComparison.OrdinalIgnoreCase) == 0) { + var meth = "test" + methodName; + if (_allCommands.ContainsKey(meth)) { + return _allCommands[meth]; + } + } + + if (methodName.IndexOf("add", StringComparison.OrdinalIgnoreCase) == 0) { + // try it with 'register' instead + var result = GetMethod("register" + methodName.Substring(3)); + if (result != null) { + return result; + } + } + + if (methodName.IndexOf("remove", StringComparison.OrdinalIgnoreCase) == 0) { + // try it with 'register' instead + var result = GetMethod("unregister" + methodName.Substring(6)); + if (result != null) { + return result; + } + } + + // can't find one, return null. + return null; + }); + + // hmm, it is possible to get the parameter types to match better when binding. + // module.ExportedFunctions.FirstOrDefault().Value.Parameters.Values.First().ParameterType + } + + internal object CallPowerShellWithoutRequest(string method, params object[] args) { + var cmdInfo = GetMethod(method); + if (cmdInfo == null) { + return null; + } + + var result = _powershell.InvokeFunction(cmdInfo.Name, null, null, args); + if (result == null) { + // failure! + throw new Exception(Messages.PowershellScriptFunctionReturnsNull.format(_module.Name, method)); + } + + return result; + } + + // lock is on this instance only + + internal void ReportErrors(PsRequest request, IEnumerable errors) { + foreach (var error in errors) { + request.Error(error.FullyQualifiedErrorId, error.CategoryInfo.Category.ToString(), error.TargetObject == null ? null : error.TargetObject.ToString(), error.ErrorDetails == null ? error.Exception.Message : error.ErrorDetails.Message); + if (!string.IsNullOrWhiteSpace(error.Exception.StackTrace)) { + // give a debug hint if we have a script stack trace. How nice of us. + // the exception stack trace gives better stack than the script stack trace + request.Debug(Constants.ScriptStackTrace, error.Exception.StackTrace); + } + } + } + + private IAsyncResult _stopResult; + private object _stopLock = new object(); + + internal void CancelRequest() { + if (!_reentrancyLock.WaitOne(0)) { + // it's running right now. +#if DEBUG + NativeMethods.OutputDebugString("[Cmdlet:debugging] -- Stopping powershell script."); +#endif + lock (_stopLock) { + if (_stopResult == null) { + _stopResult = _powershell.BeginStop(ar => { }, null); + } + } + } + } + + internal object CallPowerShell(PsRequest request, params object[] args) { + // the lock ensures that we're not re-entrant into the same powershell runspace + lock (_lock) { + if (!_reentrancyLock.WaitOne(0)) { + // this lock is set to false, meaning we're still in a call **ON THIS THREAD** + // this is bad karma -- powershell won't let us call into the runspace again + // we're going to throw an error here because this indicates that the currently + // running powershell call is calling back into PM, and it has called back + // into this provider. That's just bad bad bad. + throw new Exception("Re-entrancy Violation in powershell module"); + } + + try { + // otherwise, this is the first time we've been here during this call. + _reentrancyLock.Reset(); + + _powershell.SetVariable("request", request); + _powershell.Streams.ClearStreams(); + + object finalValue = null; + ConcurrentBag errors = new ConcurrentBag(); + + request.Debug("INVOKING PowerShell Fn {0} with args {1} that has length {2}", request.CommandInfo.Name, String.Join(", ", args), args.Length); + + var result = _powershell.InvokeFunction(request.CommandInfo.Name, + (sender, e) => output_DataAdded(sender, e, request, ref finalValue), + (sender, e) => error_DataAdded(sender, e, request, errors), + args); + + if (result == null) + { + // result is null but it does not mean that the call fails because the command may return nothing + request.Debug(Messages.PowershellScriptFunctionReturnsNull.format(_module.Name, request.CommandInfo.Name)); + } + + if (errors.Count > 0) + { + // report the error if there are any + ReportErrors(request, errors); + _powershell.Streams.Error.Clear(); + } + + return finalValue; + } catch (CmdletInvocationException cie) { + var error = cie.ErrorRecord; + request.Error(error.FullyQualifiedErrorId, error.CategoryInfo.Category.ToString(), error.TargetObject == null ? null : error.TargetObject.ToString(), error.ErrorDetails == null ? error.Exception.Message : error.ErrorDetails.Message); + } finally { + lock (_stopLock) { + if (_stopResult != null){ + _powershell.EndStop(_stopResult); + _stopResult = null; + } + } + _powershell.Clear(); + _powershell.SetVariable("request", null); + // it's ok if someone else calls into this module now. + request.Debug("Done calling powershell", request.CommandInfo.Name, _module.Name); + _reentrancyLock.Set(); + } + + return null; + } + } + + private void error_DataAdded(object sender, DataAddedEventArgs e, PsRequest request, ConcurrentBag errors) + { + PSDataCollection errorStream = sender as PSDataCollection; + + if (errorStream == null) + { + return; + } + + var error = errorStream[e.Index]; + + if (error != null) + { + // add the error so we can report them later + errors.Add(error); + } + + } + + private void output_DataAdded(object sender, DataAddedEventArgs e, PsRequest request, ref object finalValue) + { + PSDataCollection outputstream = sender as PSDataCollection; + + if (outputstream == null) + { + return; + } + + PSObject psObject = outputstream[e.Index]; + if (psObject != null) + { + var value = psObject.ImmediateBaseObject; + var y = value as Yieldable; + if (y != null) + { + // yield it to stream the result gradually + y.YieldResult(request); + } + else + { + finalValue = value; + return; + } + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Properties/AssemblyInfo.cs b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..8b776daa7 --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Properties/AssemblyInfo.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. + +[assembly: AssemblyTitle("Microsoft.PackageManagement.MetaProvider.PowerShell")] +[assembly: AssemblyDescription("PackageManagement Powershell MetaProvider")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("Microsoft.PackageManagement.MetaProvider.PowerShell")] +[assembly: AssemblyCopyright("Copyright Microsoft © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: InternalsVisibleTo("Microsoft.PackageManagement.Test")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM + +[assembly: Guid("8a16b9f4-f12a-42ab-a396-3c966b896722")] diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Request.cs b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Request.cs new file mode 100644 index 000000000..d7d53ce0f --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Request.cs @@ -0,0 +1,213 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +using Microsoft.PackageManagement.Internal.Utility.Plugin; + +namespace Microsoft.PackageManagement.MetaProvider.PowerShell.Internal { + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using System.Management.Automation; + using System.Security; + using Microsoft.PackageManagement.Internal.Api; + using Microsoft.PackageManagement.Internal.Implementation; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Internal.Utility.Async; + + using Messages = Microsoft.PackageManagement.MetaProvider.PowerShell.Resources.Messages; + + public abstract class PsRequest : Request { + internal CommandInfo CommandInfo; + private PowerShellProviderBase _provider; + + internal bool IsMethodImplemented { + get { + return CommandInfo != null; + } + } + + public IEnumerable PackageSources { + get { + var ps = Sources; + if (ps == null) { + return new string[] { + }; + } + return ps.ToArray(); + } + } + + internal IRequest Extend(params object[] objects) { + return objects.ConcatSingleItem(this).As(); + } + + internal string GetMessageStringInternal(string messageText) { + return Messages.ResourceManager.GetString(messageText); + } + + public PSCredential Credential { + get + { + if (CredentialUsername != null && CredentialPassword != null) { + return new PSCredential(CredentialUsername, CredentialPassword); + } + return null; + } + } + + private Hashtable _options; + + public Hashtable Options { + get { + if (_options == null) { + _options = new Hashtable(); + //quick and dirty, grab all four sets and merge them. + var keys = OptionKeys ?? new string[0]; + foreach (var k in keys) { + if (_options.ContainsKey(k)) { + continue; + } + var values = GetOptionValues(k).ToArray(); + if (values.Length == 1) { + if (values[0].IsTrue()) { + _options.Add(k, true); + } else if (values[0].IndexOf("SECURESTRING:", StringComparison.OrdinalIgnoreCase) == 0) { +#if !CORECLR + _options.Add(k, values[0].Substring(13).FromProtectedString("salt")); +#endif + } else { + _options.Add(k, values[0]); + } + } else { + _options.Add(k, values); + } + } + } + return _options; + } + } + + [SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed", Justification = "This is required for the PowerShell Providers.")] + public object CloneRequest(Hashtable options = null, ArrayList sources = null, PSCredential credential = null) { + var srcs = (sources ?? new ArrayList()).ToArray().Select(each => each.ToString()).ToArray(); + + options = options ?? new Hashtable(); + + var lst = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var k in options.Keys) { + if (k != null) { + var obj = options[k]; + + string[] val = null; + + if (obj is string) { + val = new[] {obj as string}; + } else { + // otherwise, try to cast it to a collection of string-like-things + var collection = obj as IEnumerable; + if (collection != null) { + val = collection.Cast().Select(each => each.ToString()).ToArray(); + } else { + // meh. ToString, and goodnight. + val = new[] {obj.ToString()}; + } + } + + lst.Add(k.ToString(), val); + } + } + + return Extend(new { + GetOptionKeys = new Func>(() => {return lst.Keys.ToArray();}), + GetOptionValues = new Func>((key) => { + if (lst.ContainsKey(key)) { + return lst[key]; + } + return new string[0]; + }), + GetSources = new Func>(() => {return srcs;}), + GetCredentialUsername = new Func(() => {return credential != null ? credential.UserName : null;}), + GetCredentialPassword = new Func(() => {return credential != null ? credential.Password: null;}), + ShouldContinueWithUntrustedPackageSource = new Func((pkgName, pkgSource) => { + // chained providers provide locations, and don't rely on 'trusted' flags from the upstream provider. + return true; + }) + }); + } + + public object CallPowerShell(params object[] args) { + if (IsMethodImplemented) { + return _provider.CallPowerShell(this, args); + } + return null; + } + + internal static PsRequest New(Object requestObject, PowerShellProviderBase provider, string methodName) { + if (requestObject is IAsyncAction) { + ((IAsyncAction)(requestObject)).OnCancel += provider.CancelRequest; + ((IAsyncAction)(requestObject)).OnAbort += provider.CancelRequest; + } + var req = requestObject.As(); + + req.CommandInfo = provider.GetMethod(methodName); + if (req.CommandInfo == null) { + req.Debug("METHOD_NOT_IMPLEMENTED", methodName); + } + req._provider = provider; + + if (req.Options == null) { + req.Debug("req.Options is null"); + + } else { + + req.Debug("Calling New() : MethodName = '{0}'", methodName); + + foreach(string key in req.Options.Keys) + { + req.Debug(String.Format(CultureInfo.CurrentCulture, "{0}: {1}", key, req.Options[key])); + } + } + + return req; + } + + public object SelectProvider(string providerName) { + return PackageManagementService.SelectProviders(providerName, Extend()).FirstOrDefault(each => each.Name.EqualsIgnoreCase(providerName)); + } + + public IEnumerable SelectProviders(string providerName) { + return PackageManagementService.SelectProviders(providerName, Extend()); + } + + + public object Services { + get { + return ProviderServices; + } + } + + public IEnumerable FindPackageByCanonicalId(string packageId, object requestObject) { + return ProviderServices.FindPackageByCanonicalId(packageId, (requestObject ?? new object()) .As()); + } + + public bool RequirePackageProvider(string packageProviderName, string minimumVersion) { + var pp = (_provider as PowerShellPackageProvider); + return PackageManagementService.RequirePackageProvider(pp == null ? Constants.ProviderNameUnknown : pp.GetPackageProviderName(), packageProviderName, minimumVersion, Extend()); + } + } +} diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/SoftwareIdentity.cs b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/SoftwareIdentity.cs new file mode 100644 index 000000000..27fbea1da --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/SoftwareIdentity.cs @@ -0,0 +1,194 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.MetaProvider.PowerShell { + using System; + using System.Collections; + using System.Linq; + using Internal; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + + public class SoftwareIdentity : Yieldable { + public SoftwareIdentity() { + } + + public SoftwareIdentity(string fastPackageReference, string name, string version, string versionScheme, string source, string summary, string searchKey, string fullPath, string filename) { + FastPackageReference = fastPackageReference; + Name = name; + Version = version; + VersionScheme = versionScheme; + Source = source; + Summary = summary; + SearchKey = searchKey; + FullPath = fullPath; + Filename = filename; + } + + public SoftwareIdentity(string fastPackageReference, string name, string version, string versionScheme, string source, string summary, string searchKey, string fullPath, string filename, Hashtable details, ArrayList entities, ArrayList links, bool fromTrustedSource) + : this(fastPackageReference, name, version, versionScheme, source, summary, searchKey,fullPath, filename) { + _details = details; + _entities = entities; + _links = links; + FromTrustedSource = fromTrustedSource; + } + + public SoftwareIdentity(string fastPackageReference, string name, string version, string versionScheme, string source, string summary, string searchKey, string fullPath, string filename, Hashtable details, ArrayList entities, ArrayList links, bool fromTrustedSource, ArrayList dependencies) + : this(fastPackageReference, name, version, versionScheme, source, summary, searchKey, fullPath, filename) { + _details = details; + _entities = entities; + _links = links; + FromTrustedSource = fromTrustedSource; + _dependencies = dependencies; + } + + public SoftwareIdentity(string fastPackageReference, string name, string version, string versionScheme, string source, string summary, string searchKey, string fullPath, string filename, Hashtable details, ArrayList entities, ArrayList links, bool fromTrustedSource, ArrayList dependencies, string tagId, string xmlLang) + : this(fastPackageReference, name, version, versionScheme, source, summary, searchKey, fullPath, filename) + { + _details = details; + _entities = entities; + _links = links; + FromTrustedSource = fromTrustedSource; + _dependencies = dependencies; + _tagId = tagId; + _xmlLang = xmlLang; + } + + public SoftwareIdentity(string fastPackageReference, string name, string version, string versionScheme, string source, string summary, string searchKey, string fullPath, string filename, Hashtable details, ArrayList entities, ArrayList links, bool fromTrustedSource, ArrayList dependencies, string tagId, string xmlLang, string destinationPath) + : this(fastPackageReference, name, version, versionScheme, source, summary, searchKey, fullPath, filename) + { + _details = details; + _entities = entities; + _links = links; + FromTrustedSource = fromTrustedSource; + _dependencies = dependencies; + _tagId = tagId; + _xmlLang = xmlLang; + _destinationPath = destinationPath; + } + + public SoftwareIdentity(string xmlSwidTag, bool commitImmediately) + { + _xmlSwidTag = xmlSwidTag; + _commitImmediately = commitImmediately; + } + + + public string FastPackageReference {get; set;} + public string Name {get; set;} + public string Version {get; set;} + public string VersionScheme {get; set;} + public string Source {get; set;} + public string Summary {get; set;} + + public string FullPath { get; set; } + public string Filename { get; set; } + + public string SearchKey {get; set;} + + public bool FromTrustedSource {get; set;} + + public override bool YieldResult(PsRequest r) { + if (r == null) { + throw new ArgumentNullException("r"); + } + + // if we get an xml swidtag string, use that + if (!string.IsNullOrWhiteSpace(_xmlSwidTag)) + { + return r.YieldSoftwareIdentityXml(_xmlSwidTag, _commitImmediately) != null; + } + + bool result = r.YieldSoftwareIdentity(FastPackageReference, Name, Version, VersionScheme, Summary, Source, SearchKey, FullPath, Filename) != null && YieldTagId(r) && YieldXmlLang(r) + && YieldDetails(r) && YieldEntities(r) && YieldLinks(r) && YieldDependencies(r) && r.AddMetadata(FastPackageReference, "FromTrustedSource", FromTrustedSource.ToString()) != null; + + // check whether we have destination + if (!string.IsNullOrWhiteSpace(_destinationPath)) + { + var payload = r.AddPayload(); + + // could not add the payload, return false + if (string.IsNullOrWhiteSpace(payload)) + { + return false; + } + + if (r.AddDirectory(payload, System.IO.Path.GetFileName(_destinationPath), System.IO.Path.GetDirectoryName(_destinationPath), null, true) == null) + { + // cannot add directory + return false; + } + } + + return result; + } + + private ArrayList _links; + private ArrayList _entities; + private ArrayList _dependencies; + private string _destinationPath; + private string _tagId; + private string _xmlLang; + private string _xmlSwidTag; + private bool _commitImmediately; + + protected virtual bool YieldTagId(PsRequest r) + { + if (string.IsNullOrWhiteSpace(_tagId)) + { + return true; + } + + return r.AddTagId(_tagId) != null; + } + + protected virtual bool YieldXmlLang(PsRequest r) + { + if (string.IsNullOrWhiteSpace(_xmlLang)) + { + return true; + } + + return r.AddCulture(_xmlLang) != null; + } + protected override bool YieldDetails(PsRequest r) { + if (_details != null && _details.Count > 0) { + // we need to send this back as a set of key/path & value pairs. + return _details.Flatten().All(kvp => r.AddMetadata(FastPackageReference,kvp.Key, kvp.Value) != null); + } + return true; + } + + protected virtual bool YieldLinks(PsRequest r) { + if( _links != null ) { + return _links.OfType().All(link => r.AddLink(new Uri(link.HRef), link.Relationship, link.MediaType, link.Ownership, link.Use, link.AppliesToMedia, link.Artifact) != null); + } + return true; + } + + protected virtual bool YieldDependencies(PsRequest r) { + if (_dependencies != null) { + return _dependencies.OfType().All(dep => r.AddDependency(dep.ProviderName, dep.PackageName, dep.Version, dep.Source, dep.AppliesTo ) != null); + } + return true; + } + + + protected virtual bool YieldEntities(PsRequest r) { + if (_links != null) { + return _entities.OfType().All(entity => r.AddEntity(entity.Name, entity.RegId, entity.Role, entity.Thumbprint) != null); + } + return true; + } + } +} diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Yieldable.cs b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Yieldable.cs new file mode 100644 index 000000000..61c6c4614 --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/Yieldable.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.MetaProvider.PowerShell.Internal { + using System.Collections; + using System.Linq; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + + public abstract class Yieldable { + protected Hashtable _details; + + public Hashtable Details { + get { + return _details ?? (_details = new Hashtable()); + } + internal set { + _details = value; + } + } + + public abstract bool YieldResult(PsRequest r); + + protected virtual bool YieldDetails(PsRequest r) { + if (_details != null && _details.Count > 0) { + // we need to send this back as a set of key/path & value pairs. + return _details.Flatten().All(kvp => r.YieldKeyValuePair(kvp.Key, kvp.Value)); + } + return true; + } + } +} diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/provider.manifest b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/provider.manifest new file mode 100644 index 000000000..91e0cf013 --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/provider.manifest @@ -0,0 +1,8 @@ + + + + diff --git a/src/Microsoft.PackageManagement.MetaProvider.PowerShell/resources/Microsoft.PackageManagement.MetaProvider.PowerShell.Resources.Messages.resx b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/resources/Microsoft.PackageManagement.MetaProvider.PowerShell.Resources.Messages.resx new file mode 100644 index 000000000..647c80f5b --- /dev/null +++ b/src/Microsoft.PackageManagement.MetaProvider.PowerShell/resources/Microsoft.PackageManagement.MetaProvider.PowerShell.Resources.Messages.resx @@ -0,0 +1,182 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Fail to import the provider '{0}' because the provider with the same name also exists from path '{1}'. + {0} provider path {1} provider path + + + Import-PackageProvider '{0}' failed. + 0-provider name or path + + + File '{0}' not found. + {0} module path + + + Import-Module failed: '{0}'. + 0- module name or path + + + The module '{0}' is already loaded. + {0} - module name + + + Cannot find provider '{0}' under the specified path. + + + 'PackageManagementProviders' key is not found in the .psd1 file under '{0}'. + {0} file path. + + + PowerShell Script '{0}' Function '{1}' returns null. + {0} script name, {1} function name + + + Unable to find provider '{0}'. Please check if the ProviderName is set to '{1}' in '{2}'. + {0} provider name, {1} provider path, {2} provider name + + + Name of the provider '{0}' is null, empty or not implemented by the provider. + 0-provider name + + + Root module {0} does not match the packagemanagementprovider name {1}. + + + Script failure at : {0} + {0} script location + + + Loaded PowerShell package provider: '[{0}]'. + + + Unable to find package provider '{0}' under the specified path. + {0} module name + + + Unable to find 'PackageProviderFunctions.psm1' at '{0}' + + + Cannot load module '{0}' because no valid provider found under the specified path. + + + Cannot find base Powershell module folder. + + \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/BitmapResource.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/BitmapResource.cs new file mode 100644 index 000000000..8569d7ab2 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/BitmapResource.cs @@ -0,0 +1,63 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.Resources +{ + using System.IO; + + /// + /// A subclass of Resource which provides specific methods for manipulating the resource data. + /// + /// + /// The resource is of type (RT_GROUPICON). + /// + internal sealed class BitmapResource : Resource + { + private const int SizeOfBitmapFileHeader = 14; // this is the sizeof(BITMAPFILEHEADER) + + /// + /// Creates a new BitmapResource object without any data. The data can be later loaded from a file. + /// + /// Name of the resource. For a numeric resource identifier, prefix the decimal number with a "#". + /// Locale of the resource + public BitmapResource(string name, int locale) + : this(name, locale, null) + { + } + + /// + /// Creates a new BitmapResource object with data. The data can be later saved to a file. + /// + /// Name of the resource. For a numeric resource identifier, prefix the decimal number with a "#". + /// Locale of the resource + /// Raw resource data + public BitmapResource(string name, int locale, byte[] data) + : base(ResourceType.Bitmap, name, locale, data) + { + } + + /// + /// Reads the bitmap from a .bmp file. + /// + /// Path to a bitmap file (.bmp). + public void ReadFromFile(string path) + { + using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read)) + { + // Move past the BITMAPFILEHEADER, and copy the rest of the bitmap as the resource data. Resource + // functions expect only the BITMAPINFO struct which exists just beyond the BITMAPFILEHEADER + // struct in bitmap files. + fs.Seek(BitmapResource.SizeOfBitmapFileHeader, SeekOrigin.Begin); + + base.Data = new byte[fs.Length - BitmapResource.SizeOfBitmapFileHeader]; + fs.Read(base.Data, 0, base.Data.Length); + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/FixedFileVersionInfo.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/FixedFileVersionInfo.cs new file mode 100644 index 000000000..6c9d8799e --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/FixedFileVersionInfo.cs @@ -0,0 +1,193 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.Resources +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.IO; + + internal class FixedFileVersionInfo + { + public FixedFileVersionInfo() + { + // Set reasonable defaults + this.signature = 0xFEEF04BD; + this.structVersion = 0x00010000; // v1.0 + this.FileVersion = new Version(0, 0, 0, 0); + this.ProductVersion = new Version(0, 0, 0, 0); + this.FileFlagsMask = VersionBuildTypes.Debug | VersionBuildTypes.Prerelease; + this.FileFlags = VersionBuildTypes.None; + this.FileOS = VersionFileOS.NT_WINDOWS32; + this.FileType = VersionFileType.Application; + this.FileSubtype = VersionFileSubtype.Unknown; + this.Timestamp = DateTime.MinValue; + } + + private uint signature; + private uint structVersion; + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public Version FileVersion + { + get + { + return this.fileVersion; + } + + set + { + if (value == null) + { + throw new InvalidOperationException(); + } + + this.fileVersion = value; + } + } + private Version fileVersion; + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public Version ProductVersion + { + get + { + return this.productVersion; + } + + set + { + if (value == null) + { + throw new InvalidOperationException(); + } + + this.productVersion = value; + } + } + private Version productVersion; + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public VersionBuildTypes FileFlagsMask + { + get { return this.fileFlagsMask; } + set { this.fileFlagsMask = value; } + } + private VersionBuildTypes fileFlagsMask; + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public VersionBuildTypes FileFlags + { + get { return this.fileFlags; } + set { this.fileFlags = value; } + } + private VersionBuildTypes fileFlags; + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public VersionFileOS FileOS + { + get { return this.fileOS; } + set { this.fileOS = value; } + } + private VersionFileOS fileOS; + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public VersionFileType FileType + { + get { return this.fileType; } + set { this.fileType = value; } + } + private VersionFileType fileType; + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public VersionFileSubtype FileSubtype + { + get { return this.fileSubtype; } + set { this.fileSubtype = value; } + } + private VersionFileSubtype fileSubtype; + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public DateTime Timestamp + { + get { return this.timestamp; } + set { this.timestamp = value; } + } + private DateTime timestamp; + + public void Read(BinaryReader reader) + { + this.signature = reader.ReadUInt32(); + this.structVersion = reader.ReadUInt32(); + this.fileVersion = UInt64ToVersion(reader.ReadUInt64()); + this.productVersion = UInt64ToVersion(reader.ReadUInt64()); + this.fileFlagsMask = (VersionBuildTypes) reader.ReadInt32(); + this.fileFlags = (VersionBuildTypes) reader.ReadInt32(); + this.fileOS = (VersionFileOS) reader.ReadInt32(); + this.fileType = (VersionFileType) reader.ReadInt32(); + this.fileSubtype = (VersionFileSubtype) reader.ReadInt32(); + this.timestamp = UInt64ToDateTime(reader.ReadUInt64()); + } + + public void Write(BinaryWriter writer) + { + writer.Write(this.signature); + writer.Write(this.structVersion); + writer.Write(VersionToUInt64(this.fileVersion)); + writer.Write(VersionToUInt64(this.productVersion)); + writer.Write((int) this.fileFlagsMask); + writer.Write((int) this.fileFlags); + writer.Write((int) this.fileOS); + writer.Write((int) this.fileType); + writer.Write((int) this.fileSubtype); + writer.Write(DateTimeToUInt64(this.timestamp)); + } + + public static explicit operator FixedFileVersionInfo(byte[] bytesValue) + { + FixedFileVersionInfo ffviValue = new FixedFileVersionInfo(); + using (BinaryReader reader = new BinaryReader(new MemoryStream(bytesValue, false))) + { + ffviValue.Read(reader); + } + return ffviValue; + } + + public static explicit operator byte[](FixedFileVersionInfo ffviValue) + { + const int FFVI_LENGTH = 52; + + byte[] bytesValue = new byte[FFVI_LENGTH]; + using (BinaryWriter writer = new BinaryWriter(new MemoryStream(bytesValue, true))) + { + ffviValue.Write(writer); + } + return bytesValue; + } + + private static Version UInt64ToVersion(ulong version) + { + return new Version((int) ((version >> 16) & 0xFFFF), (int) (version & 0xFFFF), (int) (version >> 48), (int) ((version >> 32) & 0xFFFF)); + } + private static ulong VersionToUInt64(Version version) + { + return (((ulong) (ushort) version.Major) << 16) | ((ulong) (ushort) version.Minor) + | (((ulong) (ushort) version.Build) << 48) | (((ulong) (ushort) version.Revision) << 32); + } + + private static DateTime UInt64ToDateTime(ulong dateTime) + { + return (dateTime == 0 ? DateTime.MinValue : DateTime.FromFileTime((long) dateTime)); + } + private static ulong DateTimeToUInt64(DateTime dateTime) + { + return (dateTime == DateTime.MinValue ? 0 : (ulong) dateTime.ToFileTime()); + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/GroupIconInfo.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/GroupIconInfo.cs new file mode 100644 index 000000000..5cc8c6f0c --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/GroupIconInfo.cs @@ -0,0 +1,122 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.Resources +{ + using System.IO; + + internal enum GroupIconType + { + Unknown, + Icon, + Cursor, + } + + internal struct GroupIconDirectoryInfo + { + public byte width; + public byte height; + public byte colors; + public byte reserved; + public ushort planes; + public ushort bitsPerPixel; + public uint imageSize; + public uint imageOffset; // only valid when icon group is read from .ico file. + public ushort imageIndex; // only valid when icon group is read from PE resource. + } + + internal class GroupIconInfo + { + private ushort reserved; + private GroupIconType type; + private GroupIconDirectoryInfo[] images; + + public GroupIconInfo() + { + this.images = new GroupIconDirectoryInfo[0]; + } + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public GroupIconDirectoryInfo[] DirectoryInfo { get { return this.images; } } + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void ReadFromFile(Stream stream) + { + BinaryReader reader = new BinaryReader(stream); + this.Read(reader, true); + } + + public void ReadFromResource(byte[] data) + { + using (BinaryReader reader = new BinaryReader(new MemoryStream(data, false))) + { + this.Read(reader, false); + } + } + + public byte[] GetResourceData() + { + byte[] data = null; + + using (MemoryStream stream = new MemoryStream()) + { + BinaryWriter writer = new BinaryWriter(stream); + writer.Write(this.reserved); + writer.Write((ushort)this.type); + writer.Write((ushort)this.images.Length); + for (int i = 0; i < this.images.Length; ++i) + { + writer.Write(this.images[i].width); + writer.Write(this.images[i].height); + writer.Write(this.images[i].colors); + writer.Write(this.images[i].reserved); + writer.Write(this.images[i].planes); + writer.Write(this.images[i].bitsPerPixel); + writer.Write(this.images[i].imageSize); + writer.Write(this.images[i].imageIndex); + } + + data = new byte[stream.Length]; + stream.Seek(0, SeekOrigin.Begin); + stream.Read(data, 0, data.Length); + } + + return data; + } + + private void Read(BinaryReader reader, bool readFromFile) + { + this.reserved = reader.ReadUInt16(); + this.type = (GroupIconType)reader.ReadUInt16(); + + int imageCount = reader.ReadUInt16(); + this.images = new GroupIconDirectoryInfo[imageCount]; + for (int i = 0; i < imageCount; ++i) + { + this.images[i].width = reader.ReadByte(); + this.images[i].height = reader.ReadByte(); + this.images[i].colors = reader.ReadByte(); + this.images[i].reserved = reader.ReadByte(); + this.images[i].planes = reader.ReadUInt16(); + this.images[i].bitsPerPixel = reader.ReadUInt16(); + this.images[i].imageSize = reader.ReadUInt32(); + if (readFromFile) + { + this.images[i].imageOffset = reader.ReadUInt32(); + this.images[i].imageIndex = (ushort)(i + 1); + } + else + { + this.images[i].imageIndex = reader.ReadUInt16(); + } + } + } + + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/GroupIconResource.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/GroupIconResource.cs new file mode 100644 index 000000000..7df2c6361 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/GroupIconResource.cs @@ -0,0 +1,122 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.Resources +{ + using System; + using System.Collections.Generic; + using System.IO; + + /// + /// A subclass of Resource which provides specific methods for manipulating the resource data. + /// + /// + /// The resource is of type (RT_GROUPICON). + /// + internal sealed class GroupIconResource : Resource + { + internal bool dirty; + private GroupIconInfo rawGroupIconInfo; + private List icons; + + /// + /// Creates a new GroupIconResource object without any data. The data can be later loaded from a file. + /// + /// Name of the resource. For a numeric resource identifier, prefix the decimal number with a "#". + /// Locale of the resource + public GroupIconResource(string name, int locale) + : this(name, locale, null) + { + } + + /// + /// Creates a new GroupIconResource object with data. The data can be later saved to a file. + /// + /// Name of the resource. For a numeric resource identifier, prefix the decimal number with a "#". + /// Locale of the resource + /// Raw resource data + public GroupIconResource(string name, int locale, byte[] data) + : base(ResourceType.GroupIcon, name, locale, data) + { + this.RefreshIconGroupInfo(data); + } + + /// + /// Gets or sets the raw data of the resource. The data is in the format of the RT_GROUPICON resource structure. + /// + public override byte[] Data + { + get + { + if (this.dirty) + { + base.Data = this.rawGroupIconInfo.GetResourceData(); + this.dirty = false; + } + + return base.Data; + } + set + { + this.RefreshIconGroupInfo(value); + + base.Data = value; + this.dirty = false; + } + } + + /// + /// Enumerates the the icons in the icon group. + /// + public IEnumerable Icons { get { return this.icons; } } + + /// + /// Reads the icon group from a .ico file. + /// + /// Path to an icon file (.ico). + public void ReadFromFile(string path) + { + this.rawGroupIconInfo = new GroupIconInfo(); + this.icons = new List(); + using (FileStream fs = File.Open(path, FileMode.Open, FileAccess.Read)) + { + this.rawGroupIconInfo.ReadFromFile(fs); + + // After reading the group icon info header from the file, read all the icons. + for (int i = 0; i < this.rawGroupIconInfo.DirectoryInfo.Length; ++i) + { + ushort index = this.rawGroupIconInfo.DirectoryInfo[i].imageIndex; + uint offset = this.rawGroupIconInfo.DirectoryInfo[i].imageOffset; + uint size = this.rawGroupIconInfo.DirectoryInfo[i].imageSize; + byte[] data = new byte[size]; + + fs.Seek(offset, SeekOrigin.Begin); + fs.Read(data, 0, data.Length); + + Resource resource = new Resource(ResourceType.Icon, String.Concat("#", index), this.Locale, data); + this.icons.Add(resource); + } + } + + this.dirty = true; + } + + private void RefreshIconGroupInfo(byte[] refreshData) + { + this.rawGroupIconInfo = new GroupIconInfo(); + this.icons = new List(); + if (refreshData != null) + { + this.rawGroupIconInfo.ReadFromResource(refreshData); + } + + this.dirty = true; + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/NativeMethods.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/NativeMethods.cs new file mode 100644 index 000000000..429dca1f3 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/NativeMethods.cs @@ -0,0 +1,57 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.Resources +{ + using System; + using System.Runtime.InteropServices; + + internal static class NativeMethods + { + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr LoadLibraryEx(string fileName, IntPtr hFile, uint flags); + internal const uint LOAD_LIBRARY_AS_DATAFILE = 2; + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool FreeLibrary(IntPtr module); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool EnumResourceTypes(IntPtr module, EnumResTypesProc enumFunc, IntPtr param); + [return: MarshalAs(UnmanagedType.Bool)] + internal delegate bool EnumResTypesProc(IntPtr module, IntPtr type, IntPtr param); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool EnumResourceNames(IntPtr module, IntPtr type, EnumResNamesProc enumFunc, IntPtr param); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool EnumResourceNames(IntPtr module, string type, EnumResNamesProc enumFunc, IntPtr param); + [return: MarshalAs(UnmanagedType.Bool)] + internal delegate bool EnumResNamesProc(IntPtr module, IntPtr type, IntPtr name, IntPtr param); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool EnumResourceLanguages(IntPtr module, IntPtr type, IntPtr name, EnumResLangsProc enumFunc, IntPtr param); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool EnumResourceLanguages(IntPtr module, string type, string name, EnumResLangsProc enumFunc, IntPtr param); + [return: MarshalAs(UnmanagedType.Bool)] + internal delegate bool EnumResLangsProc(IntPtr module, IntPtr type, IntPtr name, ushort langId, IntPtr param); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr FindResourceEx(IntPtr module, string type, string name, ushort langId); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr LoadResource(IntPtr module, IntPtr resourceInfo); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr LockResource(IntPtr resourceData); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern uint SizeofResource(IntPtr module, IntPtr resourceInfo); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr BeginUpdateResource(string fileName, [MarshalAs(UnmanagedType.Bool)] bool deleteExistingResources); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool UpdateResource(IntPtr updateHandle, IntPtr type, IntPtr name, ushort lcid, IntPtr data, uint dataSize); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)][return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool UpdateResource(IntPtr updateHandle, IntPtr type, string name, ushort lcid, IntPtr data, uint dataSize); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool UpdateResource(IntPtr updateHandle, string type, string name, ushort lcid, IntPtr data, uint dataSize); + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool EndUpdateResource(IntPtr updateHandle, [MarshalAs(UnmanagedType.Bool)] bool discardChanges); + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/Resource.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/Resource.cs new file mode 100644 index 000000000..04d236870 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/Resource.cs @@ -0,0 +1,245 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.Resources +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Runtime.InteropServices; + using System.Security.Permissions; + + /// + /// Represents a Win32 resource which can be loaded from and saved to a PE file. + /// + internal class Resource + { + private ResourceType type; + private string name; + private int locale; + private byte[] data; + + /// + /// Creates a new Resource object without any data. The data can be later loaded from a file. + /// + /// Type of the resource; may be one of the ResourceType constants or a user-defined type. + /// Name of the resource. For a numeric resource identifier, prefix the decimal number with a "#". + /// Locale of the resource + public Resource(ResourceType type, string name, int locale) + : this(type, name, locale, null) + { + } + + /// + /// Creates a new Resource object with data. The data can be later saved to a file. + /// + /// Type of the resource; may be one of the ResourceType constants or a user-defined type. + /// Name of the resource. For a numeric resource identifier, prefix the decimal number with a "#". + /// Locale of the resource + /// Raw resource data + public Resource(ResourceType type, string name, int locale, byte[] data) + { + if (name == null) + { + throw new ArgumentNullException("name"); + } + + this.type = type; + this.name = name; + this.locale = locale; + this.data = data; + } + + /// + /// Gets or sets the type of the resource. This may be one of the ResourceType constants + /// or a user-defined type name. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public ResourceType ResourceType + { + get { return this.type; } + set { this.type = value; } + } + + /// + /// Gets or sets the name of the resource. For a numeric resource identifier, the decimal number is prefixed with a "#". + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string Name + { + get + { + return this.name; + } + + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + this.name = value; + } + } + + /// + /// Gets or sets the locale of the resource. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public int Locale + { + get { return this.locale; } + set { this.locale = value; } + } + + /// + /// Gets or sets the raw data of the resource. + /// + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + public virtual byte[] Data + { + get { return this.data; } + set { this.data = value; } + } + + /// + /// Loads the resource data from a file. The file is searched for a resource with matching type, name, and locale. + /// + /// Win32 PE file containing the resource + [SuppressMessage("Microsoft.Security", "CA2103:ReviewImperativeSecurity")] + [SecurityPermission(SecurityAction.Assert, UnmanagedCode = true)] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void Load(string file) + { + new FileIOPermission(FileIOPermissionAccess.Read, file).Demand(); + + IntPtr module = NativeMethods.LoadLibraryEx(file, IntPtr.Zero, NativeMethods.LOAD_LIBRARY_AS_DATAFILE); + try + { + this.Load(module); + } + finally + { + NativeMethods.FreeLibrary(module); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal void Load(IntPtr module) + { + IntPtr resourceInfo = NativeMethods.FindResourceEx(module, (string) this.ResourceType, this.Name, (ushort) this.Locale); + if (resourceInfo != IntPtr.Zero) + { + uint resourceLength = NativeMethods.SizeofResource(module, resourceInfo); + IntPtr resourceData = NativeMethods.LoadResource(module, resourceInfo); + IntPtr resourcePtr = NativeMethods.LockResource(resourceData); + byte[] resourceBytes = new byte[resourceLength]; + Marshal.Copy(resourcePtr, resourceBytes, 0, resourceBytes.Length); + this.Data = resourceBytes; + } + else + { + this.Data = null; + } + } + + /// + /// Saves the resource to a file. Any existing resource data with matching type, name, and locale is overwritten. + /// + /// Win32 PE file to contain the resource + [SuppressMessage("Microsoft.Security", "CA2103:ReviewImperativeSecurity")] + [SecurityPermission(SecurityAction.Assert, UnmanagedCode = true)] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void Save(string file) + { + new FileIOPermission(FileIOPermissionAccess.AllAccess, file).Demand(); + + IntPtr updateHandle = IntPtr.Zero; + try + { + updateHandle = NativeMethods.BeginUpdateResource(file, false); + this.Save(updateHandle); + if (!NativeMethods.EndUpdateResource(updateHandle, false)) + { + int err = Marshal.GetLastWin32Error(); + throw new IOException(String.Format(CultureInfo.InvariantCulture, "Failed to save resource. Error code: {0}", err)); + } + updateHandle = IntPtr.Zero; + } + finally + { + if (updateHandle != IntPtr.Zero) + { + NativeMethods.EndUpdateResource(updateHandle, true); + } + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal void Save(IntPtr updateHandle) + { + IntPtr dataPtr = IntPtr.Zero; + try + { + int dataLength = 0; + if (this.Data != null) + { + dataLength = this.Data.Length; + dataPtr = Marshal.AllocHGlobal(dataLength); + Marshal.Copy(this.Data, 0, dataPtr, dataLength); + } + bool updateSuccess; + if (this.Name.StartsWith("#", StringComparison.Ordinal)) + { + // A numeric-named resource must be saved via the integer version of UpdateResource. + IntPtr intName = new IntPtr(Int32.Parse(this.Name.Substring(1), CultureInfo.InvariantCulture)); + updateSuccess = NativeMethods.UpdateResource(updateHandle, new IntPtr(this.ResourceType.IntegerValue), intName, (ushort) this.Locale, dataPtr, (uint) dataLength); + } + else + { + updateSuccess = NativeMethods.UpdateResource(updateHandle, (string) this.ResourceType, this.Name, (ushort) this.Locale, dataPtr, (uint) dataLength); + } + if (!updateSuccess) + { + throw new IOException("Failed to save resource. Error: " + Marshal.GetLastWin32Error()); + } + } + finally + { + if (dataPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(dataPtr); + } + } + } + + /// + /// Tests if type, name, and locale of this Resource object match another Resource object. + /// + /// Resource object to be compared + /// True if the objects represent the same resource; false otherwise. + public override bool Equals(object obj) + { + Resource res = obj as Resource; + if (res == null) return false; + return this.ResourceType == res.ResourceType && this.Name == res.Name && this.Locale == res.Locale; + } + + /// + /// Gets a hash code for this Resource object. + /// + /// Hash code generated from the resource type, name, and locale. + public override int GetHashCode() + { + return this.ResourceType.GetHashCode() ^ this.Name.GetHashCode() ^ this.Locale.GetHashCode(); + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/ResourceCollection.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/ResourceCollection.cs new file mode 100644 index 000000000..dd54e115b --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/ResourceCollection.cs @@ -0,0 +1,366 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.Resources +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Runtime.InteropServices; + using System.Security.Permissions; + + /// + /// Allows reading and editing of resource data in a Win32 PE file. + /// + /// + /// To use this class: + /// Create a new ResourceCollection + /// Locate resources for the collection by calling one of the methods + /// Load data of one or more s from a file by calling the method of the + /// Resource class, or load them all at once (more efficient) with the method of the ResourceCollection. + /// Read and/or edit data of the individual Resource objects using the methods on that class. + /// Save data of one or more s to a file by calling the method of the + /// Resource class, or save them all at once (more efficient) with the method of the ResourceCollection. + /// + /// + internal class ResourceCollection : ICollection + { + private List resources; + + /// + /// Creates a new, empty ResourceCollection. + /// + public ResourceCollection() + { + this.resources = new List(); + } + + /// + /// Locates all resources in a file, including all resource types and languages. For each located resource, + /// a instance (or subclass) is added to the collection. + /// + /// The file to be searched for resources. + /// resources could not be read from the file + [SuppressMessage("Microsoft.Security", "CA2103:ReviewImperativeSecurity")] + [SecurityPermission(SecurityAction.Assert, UnmanagedCode = true)] + public void Find(string resFile) + { + new FileIOPermission(FileIOPermissionAccess.Read, resFile).Demand(); + + this.Clear(); + + IntPtr module = NativeMethods.LoadLibraryEx(resFile, IntPtr.Zero, NativeMethods.LOAD_LIBRARY_AS_DATAFILE); + if (module == IntPtr.Zero) + { + int err = Marshal.GetLastWin32Error(); + throw new IOException(String.Format(CultureInfo.InvariantCulture, "Failed to load resource file. Error code: {0}", err)); + } + try + { + if (!NativeMethods.EnumResourceTypes(module, new NativeMethods.EnumResTypesProc(this.EnumResTypes), IntPtr.Zero)) + { + int err = Marshal.GetLastWin32Error(); + throw new IOException(String.Format(CultureInfo.InvariantCulture, "Failed to enumerate resources. Error code: {0}", err)); + } + } + finally + { + NativeMethods.FreeLibrary(module); + } + } + + /// + /// Locates all resources in a file of a given type, including all languages. For each located resource, + /// a instance (or subclass) is added to the collection. + /// + /// The file to be searched for resources. + /// The type of resource to search for; may be one of the ResourceType constants or a user-defined type. + /// resources could not be read from the file + [SuppressMessage("Microsoft.Security", "CA2103:ReviewImperativeSecurity")] + [SecurityPermission(SecurityAction.Assert, UnmanagedCode = true)] + public void Find(string resFile, ResourceType type) + { + new FileIOPermission(FileIOPermissionAccess.Read, resFile).Demand(); + + this.Clear(); + + IntPtr module = NativeMethods.LoadLibraryEx(resFile, IntPtr.Zero, NativeMethods.LOAD_LIBRARY_AS_DATAFILE); + try + { + if (!NativeMethods.EnumResourceNames(module, (string) type, new NativeMethods.EnumResNamesProc(this.EnumResNames), IntPtr.Zero)) + { + int err = Marshal.GetLastWin32Error(); + throw new IOException(String.Format(CultureInfo.InvariantCulture, "EnumResourceNames error. Error code: {0}", err)); + } + } + finally + { + NativeMethods.FreeLibrary(module); + } + } + + /// + /// Locates all resources in a file of a given type and language. For each located resource, + /// a instance (or subclass) is added to the collection. + /// + /// The file to be searched for resources. + /// The type of resource to search for; may be one of the ResourceType constants or a user-defined type. + /// The name of the resource to search for. + /// resources could not be read from the file + [SuppressMessage("Microsoft.Security", "CA2103:ReviewImperativeSecurity")] + [SecurityPermission(SecurityAction.Assert, UnmanagedCode = true)] + public void Find(string resFile, ResourceType type, string name) + { + new FileIOPermission(FileIOPermissionAccess.Read, resFile).Demand(); + + this.Clear(); + + IntPtr module = NativeMethods.LoadLibraryEx(resFile, IntPtr.Zero, NativeMethods.LOAD_LIBRARY_AS_DATAFILE); + try + { + if (!NativeMethods.EnumResourceLanguages(module, (string) type, name, new NativeMethods.EnumResLangsProc(this.EnumResLangs), IntPtr.Zero)) + { + int err = Marshal.GetLastWin32Error(); + throw new IOException(String.Format(CultureInfo.InvariantCulture, "EnumResourceLanguages error. Error code: {0}", err)); + } + } + finally + { + NativeMethods.FreeLibrary(module); + } + } + + private bool EnumResTypes(IntPtr module, IntPtr type, IntPtr param) + { + if (!NativeMethods.EnumResourceNames(module, type, new NativeMethods.EnumResNamesProc(EnumResNames), IntPtr.Zero)) + { + int err = Marshal.GetLastWin32Error(); + throw new IOException(String.Format(CultureInfo.InvariantCulture, "EnumResourceNames error! Error code: {0}", err)); + } + return true; + } + + private bool EnumResNames(IntPtr module, IntPtr type, IntPtr name, IntPtr param) + { + if (!NativeMethods.EnumResourceLanguages(module, type, name, new NativeMethods.EnumResLangsProc(EnumResLangs), IntPtr.Zero)) + { + int err = Marshal.GetLastWin32Error(); + throw new IOException(String.Format(CultureInfo.InvariantCulture, "EnumResourceLanguages error. Error code: {0}", err)); + } + return true; + } + + private bool EnumResLangs(IntPtr module, IntPtr type, IntPtr name, ushort langId, IntPtr param) + { + Resource res; + if (((int) type) == ResourceType.Version.IntegerValue) + { + res = new VersionResource(ResourceNameToString(name), langId); + } + else + { + res = new Resource(ResourceNameToString(type), ResourceNameToString(name), langId); + } + + if (!this.Contains(res)) + { + this.Add(res); + } + + return true; + } + + private static string ResourceNameToString(IntPtr resName) + { + if ((resName.ToInt64() >> 16) == 0) + { + return "#" + resName.ToString(); + } + else + { + return Marshal.PtrToStringAuto(resName); + } + } + + /// + /// For all resources in the collection, loads their data from a resource file. + /// + /// The file from which resources are loaded. + [SuppressMessage("Microsoft.Security", "CA2103:ReviewImperativeSecurity")] + [SecurityPermission(SecurityAction.Assert, UnmanagedCode = true)] + public void Load(string file) + { + new FileIOPermission(FileIOPermissionAccess.Read, file).Demand(); + + IntPtr module = NativeMethods.LoadLibraryEx(file, IntPtr.Zero, NativeMethods.LOAD_LIBRARY_AS_DATAFILE); + try + { + foreach (Resource res in this) + { + res.Load(module); + } + } + finally + { + NativeMethods.FreeLibrary(module); + } + } + + /// + /// For all resources in the collection, saves their data to a resource file. + /// + /// The file to which resources are saved. + [SuppressMessage("Microsoft.Security", "CA2103:ReviewImperativeSecurity")] + [SecurityPermission(SecurityAction.Assert, UnmanagedCode = true)] + public void Save(string file) + { + new FileIOPermission(FileIOPermissionAccess.AllAccess, file).Demand(); + + IntPtr updateHandle = IntPtr.Zero; + try + { + updateHandle = NativeMethods.BeginUpdateResource(file, false); + foreach (Resource res in this) + { + res.Save(updateHandle); + } + if (!NativeMethods.EndUpdateResource(updateHandle, false)) + { + int err = Marshal.GetLastWin32Error(); + throw new IOException(String.Format(CultureInfo.InvariantCulture, "Failed to save resource. Error {0}", err)); + } + updateHandle = IntPtr.Zero; + } + finally + { + if (updateHandle != IntPtr.Zero) + { + NativeMethods.EndUpdateResource(updateHandle, true); + } + } + } + + /// + /// Gets or sets the element at the specified index. + /// + public Resource this[int index] + { + get + { + return (Resource) this.resources[index]; + } + set + { + this.resources[index] = value; + } + } + + /// + /// Adds a new item to the collection. + /// + /// The Resource to add. + public void Add(Resource item) + { + this.resources.Add(item); + } + + /// + /// Removes an item to the collection. + /// + /// The Resource to remove. + public bool Remove(Resource item) + { + return this.resources.Remove(item); + } + + /// + /// Gets the index of an item in the collection. + /// + /// The Resource to search for. + /// The index of the item, or -1 if not found. + public int IndexOf(Resource item) + { + return this.resources.IndexOf(item); + } + + /// + /// Inserts a item into the collection. + /// + /// The insertion index. + /// The Resource to insert. + public void Insert(int index, Resource item) + { + this.resources.Insert(index, item); + } + + /// + /// Tests if the collection contains an item. + /// + /// The Resource to search for. + /// true if the item is found; false otherwise + public bool Contains(Resource item) + { + return this.resources.Contains(item); + } + + /// + /// Copies the collection into an array. + /// + /// The array to copy into. + /// The starting index in the destination array. + public void CopyTo(Resource[] array, int arrayIndex) + { + this.resources.CopyTo(array, arrayIndex); + } + + /// + /// Removes all resources from the collection. + /// + public void Clear() + { + this.resources.Clear(); + } + + /// + /// Gets the number of resources in the collection. + /// + public int Count + { + get + { + return this.resources.Count; + } + } + + /// + /// Gets an enumerator over all resources in the collection. + /// + /// + public IEnumerator GetEnumerator() + { + return this.resources.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable) this.resources).GetEnumerator(); + } + + bool ICollection.IsReadOnly + { + get + { + return false; + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/ResourceType.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/ResourceType.cs new file mode 100644 index 000000000..f6af17908 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/ResourceType.cs @@ -0,0 +1,226 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.Resources +{ + using System; + using System.Diagnostics.CodeAnalysis; + + /// + /// Represents either a standard integer resource type or a custom resource type name. + /// + internal class ResourceType + { + // Silence warnings about doc-comments + #pragma warning disable 1591 + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType None { get { return "#0"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType Cursor { get { return "#1"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType Bitmap { get { return "#2"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType Icon { get { return "#3"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType Menu { get { return "#4"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType Dialog { get { return "#5"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType String { get { return "#6"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType FontDir { get { return "#7"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType Font { get { return "#8"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType Accelerator { get { return "#9"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType RCData { get { return "#10"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType MessageTable { get { return "#11"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType GroupCursor { get { return "#12"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType GroupIcon { get { return "#14"; } } + public static ResourceType Version { get { return "#16"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType DialogInclude { get { return "#17"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType PlugPlay { get { return "#19"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType Vxd { get { return "#20"; } } + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ani")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType AniCursor { get { return "#21"; } } + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ani")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType AniIcon { get { return "#22"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType Html { get { return "#23"; } } + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static ResourceType Manifest { get { return "#24"; } } + + #pragma warning restore 1591 + + private string resourceType; + + /// + /// Creates a new resource type from a string resource name. + /// + /// String resource name, + /// or an integer resource type prefixed by a #. + public ResourceType(string resourceType) + { + if (string.IsNullOrWhiteSpace(resourceType)) + { + throw new ArgumentNullException("resourceType"); + } + + this.resourceType = resourceType; + + if (this.IsInteger && this.IntegerValue < 0) + { + throw new ArgumentOutOfRangeException("resourceType"); + } + } + + /// + /// Creates a new integer resource type. + /// + /// Integer value of a well-known resource type. + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public ResourceType(int resourceType) + : this("#" + resourceType) + { + } + + /// + /// Gets a flag indicating whether the resource type is an integer type. + /// + public bool IsInteger + { + get + { + return this.resourceType.StartsWith("#", StringComparison.Ordinal); + } + } + + /// + /// Gets the integer value of the resource type, or -1 if the resource type is not an integer. + /// + public int IntegerValue + { + get + { + int value; + if (!this.IsInteger || + !Int32.TryParse(this.resourceType.Substring(1), out value)) + { + value = -1; + } + + return value; + } + } + + /// + /// Gets a string representation of the resource type. + /// + /// The custom resource name, or the name of a well-known resource type. + public override string ToString() + { + if (this.IsInteger) + { + switch (this.IntegerValue) + { + case 0: return "None"; + case 1: return "Cursor"; + case 2: return "Bitmap"; + case 3: return "Icon"; + case 4: return "Menu"; + case 5: return "Dialog"; + case 6: return "String"; + case 7: return "FontDir"; + case 8: return "Font"; + case 9: return "Accelerator"; + case 10: return "RCData"; + case 11: return "MessageTable"; + case 12: return "GroupCursor"; + case 14: return "GroupIcon"; + case 16: return "Version"; + case 17: return "DialogInclude"; + case 19: return "PlugPlay"; + case 20: return "Vxd"; + case 21: return "AniCursor"; + case 22: return "AniIcon"; + case 23: return "Html"; + case 24: return "Manifest"; + } + } + + return this.resourceType; + } + + /// + /// Tests whether one resource type equals another object. + /// + /// Other object. + /// True if equal, else false. + public override bool Equals(object obj) + { + return this.Equals(obj as ResourceType); + } + + /// + /// Tests whether one resource type equals another. + /// + /// Other resource type. + /// True if equal, else false. + public bool Equals(ResourceType otherType) + { + return otherType != null && this.resourceType.Equals(otherType.resourceType, StringComparison.Ordinal); + } + + /// + /// Gets a hash code suitable for using the resource type as a dictionary key. + /// + /// Hash code based on the resource type string. + public override int GetHashCode() + { + return this.resourceType.GetHashCode(); + } + + /// + /// Implicitly converts a string to a ResourceType. + /// + /// String resource type to convert. + /// ResourceType object. + public static implicit operator ResourceType(string resourceType) + { + return new ResourceType(resourceType); + } + + /// + /// Explicitly converts a ResourceType to a string. + /// + /// ResourceType object to convert. + /// The resource type string. + /// + /// Unlike , this conversion does not return + /// the common name of well-known integer resource types. Therefore, + /// the returned string is suitable for passing directly to Win32 + /// resource APIs that accept resource type strings. + /// + public static explicit operator string(ResourceType resourceType) + { + return resourceType.resourceType; + } + } +} + diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/VersionEnums.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/VersionEnums.cs new file mode 100644 index 000000000..a546c71e6 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/VersionEnums.cs @@ -0,0 +1,92 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.Resources +{ + // Silence warnings about doc-comments + #pragma warning disable 1591 + using System; + using System.Diagnostics.CodeAnalysis; + + /// + /// Identifies build types of a versioned file. + /// + [Flags] + public enum VersionBuildTypes : int + { + None = 0x00, + Debug = 0x01, + Prerelease = 0x02, + Patched = 0x04, + PrivateBuild = 0x08, + InfoInferred = 0x10, + SpecialBuild = 0x20, + } + + [SuppressMessage("Microsoft.Design", "CA1027:MarkEnumsWithFlags")] + internal enum VersionFileOS : int + { + Unknown = 0x00000000, + DOS = 0x00010000, + OS216 = 0x00020000, + OS232 = 0x00030000, + NT = 0x00040000, + WINCE = 0x00050000, + WINDOWS16 = 0x00000001, + PM16 = 0x00000002, + PM32 = 0x00000003, + WINDOWS32 = 0x00000004, + DOS_WINDOWS16 = 0x00010001, + DOS_WINDOWS32 = 0x00010004, + OS216_PM16 = 0x00020002, + OS232_PM32 = 0x00030003, + NT_WINDOWS32 = 0x00040004, + } + + /// + /// Identifies the type of a versioned file. + /// + [SuppressMessage("Microsoft.Design", "CA1027:MarkEnumsWithFlags")] + public enum VersionFileType : int + { + Unknown = 0, + Application = 1, + Dll = 2, + Driver = 3, + Font = 4, + VirtualDevice = 5, + StaticLibrary = 7, + } + + /// + /// Identifies the sub-type of a versioned file. + /// + public enum VersionFileSubtype : int + { + Unknown = 0, + PrinterDriver = 1, + KeyboardDriver = 2, + LanguageDriver = 3, + DisplayDriver = 4, + MouseDriver = 5, + NetworkDriver = 6, + SystemDriver = 7, + InstallableDriver = 8, + SoundDriver = 9, + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Comm")] + CommDriver = 10, + InputMethodDriver = 11, + VersionedPrinterDriver = 12, + RasterFont = 1, + VectorFont = 2, + TrueTypeFont = 3, + } + + #pragma warning restore 1591 +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/VersionInfo.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/VersionInfo.cs new file mode 100644 index 000000000..c0335dd83 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/VersionInfo.cs @@ -0,0 +1,275 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.Resources +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.IO; + using System.Text; + + internal class VersionInfo : ICollection + { + private string key; + private bool isString; + private byte[] data; + private List children; + + public VersionInfo(string key) + : base() + { + if (key == null) + { + throw new ArgumentNullException("key"); + } + + this.key = key; + this.children = new List(); + } + + public string Key + { + get + { + return this.key; + } + + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + this.key = value; + } + } + + public bool IsString + { + get + { + return this.isString; + } + + set + { + this.isString = value; + } + } + + public byte[] Data + { + get + { + return this.data; + } + + set + { + this.data = value; + this.isString = false; + } + } + + public void Read(BinaryReader reader) + { + long basePosition = reader.BaseStream.Position; + int verInfoSize = (int) reader.ReadUInt16(); + int valueSize = (int) reader.ReadUInt16(); + bool dataIsString = (reader.ReadUInt16() != 0); + StringBuilder keyStringBuilder = new StringBuilder(); + char c; + while ((c = (char) reader.ReadUInt16()) != 0) + { + keyStringBuilder.Append(c); + } + this.Key = keyStringBuilder.ToString(); + Pad(reader, basePosition); + if (valueSize == 0) + { + this.data = null; + } + else + { + if (dataIsString) valueSize *= 2; // Count is # of chars instead of bytes + this.data = reader.ReadBytes(valueSize); + this.isString = dataIsString; + Pad(reader, basePosition); + } + + while (reader.BaseStream.Position - basePosition < verInfoSize) + { + Pad(reader, basePosition); + VersionInfo childVerInfo = new VersionInfo(""); + childVerInfo.Read(reader); + this.children.Add(childVerInfo); + } + } + + public void Write(BinaryWriter writer) + { + long basePosition = writer.BaseStream.Position; + writer.Write((ushort) this.Length); + byte[] valueBytes = this.data; + writer.Write((ushort) ((valueBytes != null ? valueBytes.Length : 0) / (this.IsString ? 2 : 1))); + writer.Write((ushort) (this.IsString ? 1 : 0)); + byte[] keyBytes = new byte[Encoding.Unicode.GetByteCount(this.Key) + 2]; + Encoding.Unicode.GetBytes(this.Key, 0, this.Key.Length, keyBytes, 0); + writer.Write(keyBytes); + Pad(writer, basePosition); + if (valueBytes != null) + { + writer.Write(valueBytes); + Pad(writer, basePosition); + } + + foreach (VersionInfo childVersionInfo in this.children) + { + Pad(writer, basePosition); + childVersionInfo.Write(writer); + } + } + + private static void Pad(BinaryReader reader, long basePosition) + { + long position = reader.BaseStream.Position; + int diff = (int) (position - basePosition) % 4; + if (diff > 0) while (diff++ < 4 && reader.BaseStream.Position < reader.BaseStream.Length) reader.ReadByte(); + } + private static void Pad(BinaryWriter writer, long basePosition) + { + long position = writer.BaseStream.Position; + int diff = (int) (position - basePosition) % 4; + if (diff > 0) while (diff++ < 4) writer.Write((byte) 0); + } + + private int Length + { + get + { + int len = 6 + Encoding.Unicode.GetByteCount(this.Key) + 2; + if (len % 4 > 0) len += (4 - len % 4); + len += (this.data != null ? this.data.Length : 0); + if (len % 4 > 0) len += (4 - len % 4); + foreach (VersionInfo childVersionInfo in this.children) + { + if (len % 4 > 0) len += (4 - len % 4); + len += childVersionInfo.Length; + } + return len; + } + } + + public static explicit operator VersionInfo(byte[] bytesValue) + { + VersionInfo viValue = new VersionInfo(""); + using (BinaryReader reader = new BinaryReader(new MemoryStream(bytesValue, false))) + { + viValue.Read(reader); + } + return viValue; + } + + public static explicit operator byte[](VersionInfo viValue) + { + byte[] bytesValue = new byte[viValue.Length]; + using (BinaryWriter writer = new BinaryWriter(new MemoryStream(bytesValue, true))) + { + viValue.Write(writer); + } + return bytesValue; + } + + public VersionInfo this[string itemKey] + { + get + { + int index = this.IndexOf(itemKey); + if (index < 0) return null; + return this.children[index]; + } + } + + public void Add(VersionInfo item) + { + this.children.Add(item); + } + + public bool Remove(VersionInfo item) + { + return this.children.Remove(item); + } + + public bool Remove(string itemKey) + { + int index = this.IndexOf(itemKey); + if (index >= 0) + { + this.children.RemoveAt(index); + return true; + } + else + { + return false; + } + } + + private int IndexOf(string itemKey) + { + for (int i = 0; i < this.children.Count; i++) + { + if (this.children[i].Key == itemKey) return i; + } + return -1; + } + + public bool Contains(VersionInfo item) + { + return this.children.Contains(item); + } + + public void CopyTo(VersionInfo[] array, int index) + { + this.children.CopyTo(array, index); + } + + public void Clear() + { + this.children.Clear(); + } + + public int Count + { + get + { + return this.children.Count; + } + } + + public bool IsReadOnly + { + get + { + return false; + } + } + + public IEnumerator GetEnumerator() + { + return this.children.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/VersionResource.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/VersionResource.cs new file mode 100644 index 000000000..c6705b055 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/VersionResource.cs @@ -0,0 +1,433 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.Resources +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + + /// + /// A subclass of Resource which provides specific methods for manipulating the resource data. + /// + /// + /// The resource is of type (RT_VERSION). + /// + [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] + internal sealed class VersionResource : Resource, ICollection + { + internal bool dirty; + private VersionInfo rawVersionInfo; + private FixedFileVersionInfo rawFileVersionInfo; + + /// + /// Creates a new VersionResource object without any data. The data can be later loaded from a file. + /// + /// Name of the resource. For a numeric resource identifier, prefix the decimal number with a "#". + /// Locale of the resource + public VersionResource(string name, int locale) + : this(name, locale, null) + { + } + + /// + /// Creates a new VersionResource object with data. The data can be later saved to a file. + /// + /// Name of the resource. For a numeric resource identifier, prefix the decimal number with a "#". + /// Locale of the resource + /// Raw resource data + public VersionResource(string name, int locale, byte[] data) + : base(ResourceType.Version, name, locale, data) + { + this.RefreshVersionInfo(data); + } + + /// + /// Gets or sets the raw data of the resource. The data is in the format of the VS_VERSIONINFO structure. + /// + public override byte[] Data + { + get + { + if (this.dirty) + { + this.rawVersionInfo.Data = (byte[]) this.rawFileVersionInfo; + base.Data = (byte[]) this.rawVersionInfo; + this.dirty = false; + } + + return base.Data; + } + set + { + base.Data = value; + this.RefreshVersionInfo(value); + this.dirty = false; + } + } + + private void RefreshVersionInfo(byte[] refreshData) + { + if (refreshData == null) + { + this.rawVersionInfo = new VersionInfo("VS_VERSION_INFO"); + this.rawFileVersionInfo = new FixedFileVersionInfo(); + } + else + { + this.rawVersionInfo = (VersionInfo) refreshData; + this.rawFileVersionInfo = (FixedFileVersionInfo) this.rawVersionInfo.Data; + } + } + + /// + /// Gets or sets the binary locale-independent file version of the version resource. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public Version FileVersion + { + get + { + return this.rawFileVersionInfo.FileVersion; + } + set + { + this.rawFileVersionInfo.FileVersion = value; + this.dirty = true; + } + } + + /// + /// Gets or sets the binary locale-independent product version of the version resource. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public Version ProductVersion + { + get + { + return this.rawFileVersionInfo.ProductVersion; + } + set + { + this.rawFileVersionInfo.ProductVersion = value; + this.dirty = true; + } + } + + /// + /// Gets or sets a bitmask that specifies the build types of the file. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public VersionBuildTypes BuildTypes + { + get + { + return this.rawFileVersionInfo.FileFlags & + this.rawFileVersionInfo.FileFlagsMask; + } + set + { + this.rawFileVersionInfo.FileFlags = value; + this.rawFileVersionInfo.FileFlagsMask = value; + this.dirty = true; + } + } + + /// + /// Gets or sets the general type of the file. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public VersionFileType FileType + { + get + { + return this.rawFileVersionInfo.FileType; + } + set + { + this.rawFileVersionInfo.FileType = value; + this.dirty = true; + } + } + + /// + /// Gets or sets the specific type of the file. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public VersionFileSubtype FileSubtype + { + get + { + return this.rawFileVersionInfo.FileSubtype; + } + set + { + this.rawFileVersionInfo.FileSubtype = value; + this.dirty = true; + } + } + + /// + /// Gets or sets the binary creation date and time. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public DateTime Timestamp + { + get + { + return this.rawFileVersionInfo.Timestamp; + } + set + { + this.rawFileVersionInfo.Timestamp = value; + this.dirty = true; + } + } + + /// + /// Gets the string table for a specific locale, or null if there is no table for that locale. + /// + /// + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public VersionStringTable this[int locale] + { + get + { + VersionInfo svi = this.rawVersionInfo["StringFileInfo"]; + if (svi != null) + { + foreach (VersionInfo strings in svi) + { + int stringsLocale = UInt16.Parse(strings.Key.Substring(0, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture); + if (stringsLocale == locale) + { + return new VersionStringTable(this, strings); + } + } + } + return null; + } + } + + /// + /// Adds a new version string table for a locale. + /// + /// Locale of the table + /// The new string table, or the existing table if the locale already existed. + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public VersionStringTable Add(int locale) + { + VersionInfo svi = this.rawVersionInfo["StringFileInfo"]; + if (svi == null) + { + svi = new VersionInfo("StringFileInfo"); + this.rawVersionInfo.Add(svi); + } + foreach (VersionInfo strings in svi) + { + int stringsLocale = UInt16.Parse(strings.Key.Substring(0, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture); + if (stringsLocale == locale) + { + return new VersionStringTable(this, strings); + } + } + + VersionInfo newStrings = new VersionInfo( + ((ushort) locale).ToString("x4", CultureInfo.InvariantCulture) + ((ushort) 1200).ToString("x4", CultureInfo.InvariantCulture)); + svi.Add(newStrings); + this.dirty = true; + + VersionInfo vvi = this.rawVersionInfo["VarFileInfo"]; + if (vvi == null) + { + vvi = new VersionInfo("VarFileInfo"); + vvi.Add(new VersionInfo("Translation")); + this.rawVersionInfo.Add(vvi); + } + VersionInfo tVerInfo = vvi["Translation"]; + if (tVerInfo != null) + { + byte[] oldValue = tVerInfo.Data; + if (oldValue == null) oldValue = new byte[0]; + tVerInfo.Data = new byte[oldValue.Length + 4]; + Array.Copy(oldValue, tVerInfo.Data, oldValue.Length); + using (BinaryWriter bw = new BinaryWriter(new MemoryStream(tVerInfo.Data, oldValue.Length, 4, true))) + { + bw.Write((ushort) locale); + bw.Write((ushort) 1200); + } + } + + return new VersionStringTable(this, newStrings); + } + + /// + /// Removes a version string table for a locale. + /// + /// Locale of the table + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void Remove(int locale) + { + VersionInfo svi = this.rawVersionInfo["StringFileInfo"]; + if (svi != null) + { + foreach (VersionInfo strings in svi) + { + int stringsLocale = UInt16.Parse(strings.Key.Substring(0, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture); + if (stringsLocale == locale) + { + svi.Remove(strings); + this.dirty = true; + break; + } + } + + } + + VersionInfo vvi = this.rawVersionInfo["VarFileInfo"]; + if (vvi != null) + { + VersionInfo tVerInfo = vvi["Translation"]; + if (tVerInfo != null) + { + byte[] newValue = new byte[tVerInfo.Data.Length]; + int j = 0; + using (BinaryWriter bw = new BinaryWriter(new MemoryStream(newValue, 0, newValue.Length, true))) + { + using (BinaryReader br = new BinaryReader(new MemoryStream(tVerInfo.Data))) + { + for (int i = tVerInfo.Data.Length / 4; i > 0; i--) + { + ushort tLocale = br.ReadUInt16(); + ushort cp = br.ReadUInt16(); + if (tLocale != locale) + { + bw.Write((ushort) tLocale); + bw.Write((ushort) cp); + j++; + } + } + } + } + tVerInfo.Data = new byte[j * 4]; + Array.Copy(newValue, tVerInfo.Data, tVerInfo.Data.Length); + } + } + } + + /// + /// Checks if a version string table exists for a given locale. + /// + /// Locale to search for + /// True if a string table was found for the locale; false otherwise. + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool Contains(int locale) + { + return this[locale] != null; + } + + /// + /// Gets the number string tables in the version resource. + /// + public int Count + { + get + { + VersionInfo svi = this.rawVersionInfo["StringFileInfo"]; + return svi != null ? svi.Count : 0; + } + } + + /// + /// Removes all string tables from the version resource. + /// + public void Clear() + { + VersionInfo svi = this.rawVersionInfo["StringFileInfo"]; + if (svi != null) + { + svi.Clear(); + this.dirty = true; + } + } + + bool ICollection.IsReadOnly + { + get + { + return false; + } + } + + void ICollection.Add(VersionStringTable item) + { + throw new NotSupportedException(); + } + + bool ICollection.Remove(VersionStringTable item) + { + throw new NotSupportedException(); + } + + bool ICollection.Contains(VersionStringTable item) + { + throw new NotSupportedException(); + } + + /// + /// Copies the version string tables to an array, starting at a particular array index. + /// + /// The one-dimensional Array that is the destination of the elements copied + /// from the collection. The Array must have zero-based indexing. + /// The zero-based index in array at which copying begins. + public void CopyTo(VersionStringTable[] array, int arrayIndex) + { + if (array == null) { + throw new ArgumentNullException("array"); + } + VersionInfo svi = this.rawVersionInfo["StringFileInfo"]; + if (svi != null) + { + foreach (VersionInfo strings in svi) + { + array[arrayIndex++] = new VersionStringTable(this, strings); + } + } + } + + /// + /// Gets an enumerator that can iterate over the version string tables in the collection. + /// + /// An enumerator that returns objects. + public IEnumerator GetEnumerator() + { + VersionInfo svi = this.rawVersionInfo["StringFileInfo"]; + if (svi != null) + { + foreach (VersionInfo strings in svi) + { + yield return new VersionStringTable(this, strings); + } + } + } + + /// + /// Gets an enumerator that can iterate over the version string tables in the collection. + /// + /// An enumerator that returns objects. + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/VersionStringTable.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/VersionStringTable.cs new file mode 100644 index 000000000..ad0b8ad98 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/Resources/VersionStringTable.cs @@ -0,0 +1,237 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.Resources +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Text; + + /// + /// Represents a string table of a file version resource. + /// + [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] + internal sealed class VersionStringTable : IDictionary + { + private VersionResource parent; + private VersionInfo rawStringVersionInfo; + + internal VersionStringTable(VersionResource parent, VersionInfo rawStringVersionInfo) + { + this.parent = parent; + this.rawStringVersionInfo = rawStringVersionInfo; + } + + /// + /// Gets the locale (LCID) of the string table. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public int Locale + { + get + { + return UInt16.Parse(rawStringVersionInfo.Key.Substring(0, 4), NumberStyles.HexNumber, CultureInfo.InvariantCulture); + } + set + { + rawStringVersionInfo.Key = ((ushort) value).ToString("x4", CultureInfo.InvariantCulture) + rawStringVersionInfo.Key.Substring(4, 4); + this.parent.dirty = true; + } + } + + /// + /// Gets or sets a string value. + /// + /// Name of the string. + public string this[string key] + { + get + { + VersionInfo verValue = this.rawStringVersionInfo[key]; + if (verValue == null) + { + return null; + } + else + { + return Encoding.Unicode.GetString(verValue.Data, 0, verValue.Data.Length - 2); + } + } + set + { + if (value == null) + { + rawStringVersionInfo.Remove(key); + } + else + { + VersionInfo verValue = rawStringVersionInfo[key]; + if (verValue == null) + { + verValue = new VersionInfo(key); + verValue.IsString = true; + rawStringVersionInfo.Add(verValue); + } + verValue.Data = new byte[Encoding.Unicode.GetByteCount(value) + 2]; + Encoding.Unicode.GetBytes(value, 0, value.Length, verValue.Data, 0); + } + this.parent.dirty = true; + } + } + + bool ICollection>.IsReadOnly + { + get + { + return false; + } + } + + bool IDictionary.TryGetValue(string key, out string value) + { + value = this[key]; + return value != null; + } + + + void ICollection>.Add(KeyValuePair item) + { + this[item.Key] = item.Value; + } + + bool ICollection>.Remove(KeyValuePair item) + { + string value = this[item.Key]; + if (value == item.Value) + { + this[item.Key] = null; + return true; + } + else + { + return false; + } + } + + bool ICollection>.Contains(KeyValuePair item) + { + string value = this[item.Key]; + if (value == item.Value) + { + return true; + } + else + { + return false; + } + } + + bool IDictionary.ContainsKey(string key) + { + return this[key] != null; + } + + void IDictionary.Add(string key, string value) + { + this[key] = value; + } + + bool IDictionary.Remove(string key) + { + if (this[key] != null) + { + this[key] = null; + return true; + } + else + { + return false; + } + } + + /// + /// Removes all strings from the string table. + /// + public void Clear() + { + this.rawStringVersionInfo.Clear(); + } + + /// + /// Gets a collection of all the names of the strings in the table. + /// + public ICollection Keys + { + get + { + List keys = new List(this.rawStringVersionInfo.Count); + foreach (VersionInfo verValue in this.rawStringVersionInfo) + { + keys.Add(verValue.Key); + } + return keys; + } + } + + /// + /// Gets a collection of all the values in the table. + /// + public ICollection Values + { + get + { + List values = new List(this.rawStringVersionInfo.Count); + foreach (VersionInfo verValue in this.rawStringVersionInfo) + { + values.Add(Encoding.Unicode.GetString(verValue.Data, 0, verValue.Data.Length - 2)); + } + return values; + } + } + + /// + /// Gets the number of strings in the table. + /// + public int Count + { + get + { + return this.rawStringVersionInfo.Count; + } + } + + void ICollection>.CopyTo(KeyValuePair[] array, int index) + { + foreach (VersionInfo verValue in this.rawStringVersionInfo) + { + array[index++] = new KeyValuePair(verValue.Key, Encoding.Unicode.GetString(verValue.Data, 0, verValue.Data.Length - 2)); + } + } + + /// + /// Gets an enumeration over all strings in the table. + /// + /// Enumeration of string name and value pairs + public IEnumerator> GetEnumerator() + { + foreach (VersionInfo verValue in this.rawStringVersionInfo) + { + yield return new KeyValuePair(verValue.Key, Encoding.Unicode.GetString(verValue.Data, 0, verValue.Data.Length - 2)); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/Attributes.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/Attributes.cs new file mode 100644 index 000000000..5dc169bb6 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/Attributes.cs @@ -0,0 +1,67 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller.Linq +{ + using System; + + /// + /// Apply to a subclass of QRecord to indicate the name of + /// the table the record type is to be used with. + /// + /// + /// If this attribute is not used on a record type, the default + /// table name will be derived from the record type name. (An + /// optional underscore suffix is stripped.) + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + internal class DatabaseTableAttribute : Attribute + { + /// + /// Creates a new DatabaseTableAttribute for the specified table. + /// + /// name of the table associated with the record type + public DatabaseTableAttribute(string table) + { + this.Table = table; + } + + /// + /// Gets or sets the table associated with the record type. + /// + public string Table { get; set; } + } + + /// + /// Apply to a property on a subclass of QRecord to indicate + /// the name of the column the property is to be associated with. + /// + /// + /// If this attribute is not used on a property, the default + /// column name will be the same as the property name. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + internal class DatabaseColumnAttribute : Attribute + { + /// + /// Creates a new DatabaseColumnAttribute which maps a + /// record property to a column. + /// + /// name of the column associated with the property + public DatabaseColumnAttribute(string column) + { + this.Column = column; + } + + /// + /// Gets or sets the column associated with the record property. + /// + public string Column { get; set; } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/Entities.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/Entities.cs new file mode 100644 index 000000000..d9bfcb79d --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/Entities.cs @@ -0,0 +1,157 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller.Linq +{ + // Silence warnings about style and doc-comments + #if !CODE_ANALYSIS + #pragma warning disable 1591 + #region Generated code + + internal class Component_ : QRecord + { + public string Component { get { return this[0]; } set { this[0] = value; } } + public string ComponentId { get { return this[1]; } set { this[1] = value; } } + public string Directory_ { get { return this[2]; } set { this[2] = value; } } + public string Condition { get { return this[4]; } set { this[4] = value; } } + public string KeyPath { get { return this[5]; } set { this[5] = value; } } + public ComponentAttributes Attributes + { get { return (ComponentAttributes) this.I(3); } set { this[3] = ((int) value).ToString(); } } + } + + internal class CreateFolder_ : QRecord + { + public string Directory_ { get { return this[0]; } set { this[0] = value; } } + public string Component_ { get { return this[1]; } set { this[1] = value; } } + } + + internal class CustomAction_ : QRecord + { + public string Action { get { return this[0]; } set { this[0] = value; } } + public string Source { get { return this[2]; } set { this[2] = value; } } + public string Target { get { return this[3]; } set { this[3] = value; } } + public CustomActionTypes Type + { get { return (CustomActionTypes) this.I(1); } set { this[1] = ((int) value).ToString(); } } + } + + internal class Directory_ : QRecord + { + public string Directory { get { return this[0]; } set { this[0] = value; } } + public string Directory_Parent { get { return this[1]; } set { this[1] = value; } } + public string DefaultDir { get { return this[2]; } set { this[2] = value; } } + } + + internal class DuplicateFile_ : QRecord + { + public string FileKey { get { return this[0]; } set { this[0] = value; } } + public string Component_ { get { return this[1]; } set { this[1] = value; } } + public string File_ { get { return this[2]; } set { this[2] = value; } } + public string DestName { get { return this[4]; } set { this[4] = value; } } + public string DestFolder { get { return this[5]; } set { this[5] = value; } } + } + + internal class Feature_ : QRecord + { + public string Feature { get { return this[0]; } set { this[0] = value; } } + public string Feature_Parent { get { return this[1]; } set { this[1] = value; } } + public string Title { get { return this[2]; } set { this[2] = value; } } + public string Description { get { return this[3]; } set { this[3] = value; } } + public int? Display { get { return this.NI(4); } set { this[4] = value.ToString(); } } + public int Level { get { return this.I(5); } set { this[5] = value.ToString(); } } + public string Directory_ { get { return this[6]; } set { this[6] = value; } } + public FeatureAttributes Attributes + { get { return (FeatureAttributes) this.I(7); } set { this[7] = ((int) value).ToString(); } } + } + + [DatabaseTable("FeatureComponents")] + internal class FeatureComponent_ : QRecord + { + public string Feature_ { get { return this[0]; } set { this[0] = value; } } + public string Component_ { get { return this[1]; } set { this[1] = value; } } + } + + internal class File_ : QRecord + { + public string File { get { return this[0]; } set { this[0] = value; } } + public string Component_ { get { return this[1]; } set { this[1] = value; } } + public string FileName { get { return this[2]; } set { this[2] = value; } } + public int FileSize { get { return this.I(3); } set { this[3] = value.ToString(); } } + public string Version { get { return this[4]; } set { this[4] = value; } } + public string Language { get { return this[5]; } set { this[5] = value; } } + public int Sequence { get { return this.I(7); } set { this[7] = value.ToString(); } } + public FileAttributes Attributes + { get { return (FileAttributes) this.I(6); } set { this[6] = ((int) value).ToString(); } } + } + + [DatabaseTable("MsiFileHash")] + internal class FileHash_ : QRecord + { + public string File_ { get { return this[0]; } set { this[0] = value; } } + public int Options { get { return this.I(1); } set { this[1] = value.ToString(); } } + public int HashPart1 { get { return this.I(2); } set { this[2] = value.ToString(); } } + public int HashPart2 { get { return this.I(3); } set { this[3] = value.ToString(); } } + public int HashPart3 { get { return this.I(4); } set { this[4] = value.ToString(); } } + public int HashPart4 { get { return this.I(5); } set { this[5] = value.ToString(); } } + } + + [DatabaseTable("InstallExecuteSequence")] + internal class InstallSequence_ : QRecord + { + public string Action { get { return this[0]; } set { this[0] = value; } } + public string Condition { get { return this[1]; } set { this[1] = value; } } + public int Sequence { get { return this.I(2); } set { this[2] = value.ToString(); } } + } + + internal class LaunchCondition_ : QRecord + { + public string Condition { get { return this[0]; } set { this[0] = value; } } + public string Description { get { return this[1]; } set { this[1] = value; } } + } + + internal class Media_ : QRecord + { + public int DiskId { get { return this.I(0); } set { this[0] = value.ToString(); } } + public int LastSequence { get { return this.I(1); } set { this[1] = value.ToString(); } } + public string DiskPrompt { get { return this[2]; } set { this[2] = value; } } + public string Cabinet { get { return this[3]; } set { this[3] = value; } } + public string VolumeLabel { get { return this[4]; } set { this[4] = value; } } + public string Source { get { return this[5]; } set { this[5] = value; } } + } + + internal class Property_ : QRecord + { + public string Property { get { return this[0]; } set { this[0] = value; } } + public string Value { get { return this[1]; } set { this[1] = value; } } + } + + internal class Registry_ : QRecord + { + public string Registry { get { return this[0]; } set { this[0] = value; } } + public string Key { get { return this[2]; } set { this[2] = value; } } + public string Name { get { return this[3]; } set { this[3] = value; } } + public string Value { get { return this[4]; } set { this[4] = value; } } + public string Component_ { get { return this[5]; } set { this[5] = value; } } + public RegistryRoot Root + { get { return (RegistryRoot) this.I(1); } set { this[0] = ((int) value).ToString(); } } + } + + internal class RemoveFile_ : QRecord + { + public string FileKey { get { return this[0]; } set { this[0] = value; } } + public string Component_ { get { return this[2]; } set { this[2] = value; } } + public string FileName { get { return this[3]; } set { this[3] = value; } } + public string DirProperty { get { return this[4]; } set { this[4] = value; } } + public RemoveFileModes InstallMode + { get { return (RemoveFileModes) this.I(5); } set { this[5] = ((int) value).ToString(); } } + } + + #endregion // Generated code + #pragma warning restore 1591 + #endif // !CODE_ANALYSIS +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/QDatabase.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/QDatabase.cs new file mode 100644 index 000000000..b986352cf --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/QDatabase.cs @@ -0,0 +1,226 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller.Linq +{ + using System; + using System.IO; + + /// + /// Allows any Database instance to be converted into a queryable database. + /// + internal static class Queryable + { + /// + /// Converts any Database instance into a queryable database. + /// + /// + /// Queryable database instance that operates on the same + /// MSI handle. + /// + /// This extension method is meant for convenient on-the-fly conversion. + /// If the existing database instance already happens to be a QDatabase, + /// then it is returned unchanged. Otherwise since the new database + /// carries the same MSI handle, only one of the instances needs to be + /// closed, not both. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static QDatabase AsQueryable(this Database db) + { + QDatabase qdb = db as QDatabase; + if (qdb == null && db != null) + { + qdb = new QDatabase(db.Handle, true, db.FilePath, db.OpenMode); + } + return qdb; + } + } + + /// + /// Queryable MSI database - extends the base Database class with + /// LINQ query functionality along with predefined entity types + /// for common tables. + /// + internal class QDatabase : Database + { + /// + /// Opens an existing database in read-only mode. + /// + /// Path to the database file. + /// the database could not be created/opened + /// + /// Because this constructor initiates database access, it cannot be used with a + /// running installation. + /// The Database object should be d after use. + /// The finalizer will close the handle if it is still open, however due to the nondeterministic + /// nature of finalization it is best that the handle be closed manually as soon as it is no + /// longer needed, as leaving lots of unused handles open can degrade performance. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public QDatabase(string filePath) + : base(filePath) + { + } + + /// + /// Opens an existing database with another database as output. + /// + /// Path to the database to be read. + /// Open mode for the database + /// Database object representing the created or opened database + /// the database could not be created/opened + /// + /// When a database is opened as the output of another database, the summary information stream + /// of the output database is actually a read-only mirror of the original database and thus cannot + /// be changed. Additionally, it is not persisted with the database. To create or modify the + /// summary information for the output database it must be closed and re-opened. + /// The returned Database object should be d after use. + /// The finalizer will close the handle if it is still open, however due to the nondeterministic + /// nature of finalization it is best that the handle be closed manually as soon as it is no + /// longer needed, as leaving lots of unused handles open can degrade performance. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public QDatabase(string filePath, string outputPath) + : base(filePath, outputPath) + { + } + + /// + /// Opens an existing database or creates a new one. + /// + /// Path to the database file. If an empty string + /// is supplied, a temporary database is created that is not persisted. + /// Open mode for the database + /// the database could not be created/opened + /// + /// To make and save changes to a database first open the database in transaction, + /// create or, or direct mode. After making the changes, always call the Commit method + /// before closing the database handle. The Commit method flushes all buffers. + /// Always call the Commit method on a database that has been opened in direct + /// mode before closing the database. Failure to do this may corrupt the database. + /// Because this constructor initiates database access, it cannot be used with a + /// running installation. + /// The Database object should be d after use. + /// The finalizer will close the handle if it is still open, however due to the nondeterministic + /// nature of finalization it is best that the handle be closed manually as soon as it is no + /// longer needed, as leaving lots of unused handles open can degrade performance. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public QDatabase(string filePath, DatabaseOpenMode mode) + : base(filePath, mode) + { + } + + /// + /// Creates a new database from an MSI handle. + /// + /// Native MSI database handle. + /// True if the handle should be closed + /// when the database object is disposed + /// Path of the database file, if known + /// Mode the handle was originally opened in + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + protected internal QDatabase( + IntPtr handle, bool ownsHandle, string filePath, DatabaseOpenMode openMode) + : base(handle, ownsHandle, filePath, openMode) + { + } + + /// + /// Gets or sets a log where all MSI SQL queries are written. + /// + /// + /// The log can be useful for debugging, or simply to watch the LINQ magic in action. + /// + public TextWriter Log { get; set; } + + /// + /// Gets a queryable table from the datbaase. + /// + /// name of the table + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public QTable this[string table] + { + get + { + return new QTable(this, table); + } + } + + #if !CODE_ANALYSIS + #region Queryable tables + + /// Queryable standard table with predefined specialized record type. + public QTable Components + { get { return new QTable(this); } } + + /// Queryable standard table with predefined specialized record type. + public QTable CreateFolders + { get { return new QTable(this); } } + + /// Queryable standard table with predefined specialized record type. + public QTable CustomActions + { get { return new QTable(this); } } + + /// Queryable standard table with predefined specialized record type. + public QTable Directories + { get { return new QTable(this); } } + + /// Queryable standard table with predefined specialized record type. + public QTable DuplicateFiles + { get { return new QTable(this); } } + + /// Queryable standard table with predefined specialized record type. + public QTable Features + { get { return new QTable(this); } } + + /// Queryable standard table with predefined specialized record type. + public QTable FeatureComponents + { get { return new QTable(this); } } + + /// Queryable standard table with predefined specialized record type. + public QTable Files + { get { return new QTable(this); } } + + /// Queryable standard table with predefined specialized record type. + public QTable FileHashes + { get { return new QTable(this); } } + + /// Queryable standard table with predefined specialized record type. + public QTable InstallExecuteSequences + { get { return new QTable(this, "InstallExecuteSequence"); } } + + /// Queryable standard table with predefined specialized record type. + public QTable InstallUISequences + { get { return new QTable(this, "InstallUISequence"); } } + + /// Queryable standard table with predefined specialized record type. + public QTable LaunchConditions + { get { return new QTable(this); } } + + /// Queryable standard table with predefined specialized record type. + public QTable Medias + { get { return new QTable(this); } } + + /// Queryable standard table with predefined specialized record type. + public QTable Properties + { get { return new QTable(this); } } + + /// Queryable standard table with predefined specialized record type. + public QTable Registries + { get { return new QTable(this); } } + + /// Queryable standard table with predefined specialized record type. + public QTable RemoveFiles + { get { return new QTable(this); } } + + #endregion // Queryable tables + #endif // !CODE_ANALYSIS + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/QRecord.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/QRecord.cs new file mode 100644 index 000000000..2b39e5ca3 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/QRecord.cs @@ -0,0 +1,508 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller.Linq +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Text; + + /// + /// Generic record entity for queryable databases, + /// and base for strongly-typed entity subclasses. + /// + /// + /// Several predefined specialized subclasses are provided for common + /// standard tables. Subclasses for additional standard tables + /// or custom tables are not necessary, but they are easy to create + /// and make the coding experience much nicer. + /// When creating subclasses, the following attributes may be + /// useful: , + /// + /// + internal class QRecord + { + /// + /// Do not call. Use QTable.NewRecord() instead. + /// + /// + /// Subclasses must also provide a public parameterless constructor. + /// QRecord constructors are only public due to implementation + /// reasons (to satisfy the new() constraint on the QTable generic + /// class). They are not intended to be called by user code other than + /// a subclass constructor. If the constructor is invoked directly, + /// the record instance will not be properly initialized (associated + /// with a database table) and calls to methods on the instance + /// will throw a NullReferenceException. + /// + /// + public QRecord() + { + } + + internal QDatabase Database { get; set; } + + internal TableInfo TableInfo { get; set; } + + internal IList Values { get; set; } + + internal bool Exists { get; set; } + + /// + /// Gets the number of fields in the record. + /// + public int FieldCount + { + get + { + return this.Values.Count; + } + } + + /// + /// Gets or sets a record field. + /// + /// column name of the field + /// + /// Setting a field value will automatically update the database. + /// + public string this[string field] + { + get + { + if (field == null) + { + throw new ArgumentNullException("field"); + } + + int index = this.TableInfo.Columns.IndexOf(field); + if (index < 0) + { + throw new ArgumentOutOfRangeException("field"); + } + + return this[index]; + } + + set + { + if (field == null) + { + throw new ArgumentNullException("field"); + } + + this.Update(new string[] { field }, new string[] { value }); + } + } + + /// + /// Gets or sets a record field. + /// + /// zero-based column index of the field + /// + /// Setting a field value will automatically update the database. + /// + public string this[int index] + { + get + { + if (index < 0 || index >= this.FieldCount) + { + throw new ArgumentOutOfRangeException("index"); + } + + return this.Values[index]; + } + + set + { + if (index < 0 || index >= this.FieldCount) + { + throw new ArgumentOutOfRangeException("index"); + } + + this.Update(new int[] { index }, new string[] { value }); + } + } + + /// + /// Used by subclasses to get a field as an integer. + /// + /// zero-based column index of the field + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "I")] + protected int I(int index) + { + string value = this[index]; + return value.Length > 0 ? + Int32.Parse(value, CultureInfo.InvariantCulture) : 0; + } + + /// + /// Used by subclasses to get a field as a nullable integer. + /// + /// zero-based column index of the field + protected int? NI(int index) + { + string value = this[index]; + return value.Length > 0 ? + new int?(Int32.Parse(value, CultureInfo.InvariantCulture)) : null; + } + + /// + /// Dumps all record fields to a string. + /// + public override string ToString() + { + StringBuilder buf = new StringBuilder(this.GetType().Name); + buf.Append(" {"); + for (int i = 0; i < this.FieldCount; i++) + { + buf.AppendFormat("{0} {1} = {2}", + (i > 0 ? "," : String.Empty), + this.TableInfo.Columns[i].Name, + this[i]); + } + buf.Append(" }"); + return buf.ToString(); + } + + /// + /// Update multiple fields in the record (and the database). + /// + /// column names of fields to update + /// new values for each field being updated + public void Update(IList fields, IList values) + { + if (fields == null) + { + throw new ArgumentNullException("fields"); + } + + if (values == null) + { + throw new ArgumentNullException("values"); + } + + if (fields.Count == 0 || values.Count == 0 || + fields.Count > this.FieldCount || + values.Count != fields.Count) + { + throw new ArgumentOutOfRangeException("fields"); + } + + int[] indexes = new int[fields.Count]; + for (int i = 0; i < indexes.Length; i++) + { + if (fields[i] == null) + { + throw new ArgumentNullException("fields[" + i + "]"); + } + + indexes[i] = this.TableInfo.Columns.IndexOf(fields[i]); + + if (indexes[i] < 0) + { + throw new ArgumentOutOfRangeException("fields[" + i + "]"); + } + } + + this.Update(indexes, values); + } + + /// + /// Update multiple fields in the record (and the database). + /// + /// column indexes of fields to update + /// new values for each field being updated + /// + /// The record (primary keys) must already exist in the table. + /// Updating primary key fields is not yet implemented; use Delete() + /// and Insert() instead. + /// + public void Update(IList indexes, IList values) + { + if (indexes == null) + { + throw new ArgumentNullException("indexes"); + } + + if (values == null) + { + throw new ArgumentNullException("values"); + } + + if (indexes.Count == 0 || values.Count == 0 || + indexes.Count > this.FieldCount || + values.Count != indexes.Count) + { + throw new ArgumentOutOfRangeException("indexes"); + } + + bool primaryKeyChanged = false; + for (int i = 0; i < indexes.Count; i++) + { + int index = indexes[i]; + if (index < 0 || index >= this.FieldCount) + { + throw new ArgumentOutOfRangeException("index[" + i + "]"); + } + + ColumnInfo col = this.TableInfo.Columns[index]; + if (this.TableInfo.PrimaryKeys.Contains(col.Name)) + { + if (values[i] == null) + { + throw new ArgumentNullException("values[" + i + "]"); + } + + primaryKeyChanged = true; + } + else if (values[i] == null) + { + if (col.IsRequired) + { + throw new ArgumentNullException("values[" + i + "]"); + } + } + + this.Values[index] = values[i]; + } + + if (this.Exists) + { + if (!primaryKeyChanged) + { + int updateRecSize = indexes.Count + this.TableInfo.PrimaryKeys.Count; + using (Record updateRec = this.Database.CreateRecord(updateRecSize)) + { + StringBuilder s = new StringBuilder("UPDATE `"); + s.Append(this.TableInfo.Name); + s.Append("` SET"); + + for (int i = 0; i < indexes.Count; i++) + { + ColumnInfo col = this.TableInfo.Columns[indexes[i]]; + if (col.Type == typeof(Stream)) + { + throw new NotSupportedException( + "Cannot update stream columns via QRecord."); + } + + int index = indexes[i]; + s.AppendFormat("{0} `{1}` = ?", + (i > 0 ? "," : String.Empty), + col.Name); + + if (values[i] != null) + { + updateRec[i + 1] = values[i]; + } + } + + for (int i = 0; i < this.TableInfo.PrimaryKeys.Count; i++) + { + string key = this.TableInfo.PrimaryKeys[i]; + s.AppendFormat(" {0} `{1}` = ?", (i == 0 ? "WHERE" : "AND"), key); + int index = this.TableInfo.Columns.IndexOf(key); + updateRec[indexes.Count + i + 1] = this.Values[index]; + + } + + string updateSql = s.ToString(); + TextWriter log = this.Database.Log; + if (log != null) + { + log.WriteLine(); + log.WriteLine(updateSql); + for (int field = 1; field <= updateRecSize; field++) + { + log.WriteLine(" ? = " + updateRec.GetString(field)); + } + } + + this.Database.Execute(updateSql, updateRec); + } + } + else + { + throw new NotImplementedException( + "Update() cannot handle changed primary keys yet."); + // TODO: + // query using old values + // update values + // View.Replace + } + } + } + + /// + /// Inserts the record in the database. + /// + /// + /// The record (primary keys) may not already exist in the table. + /// Use to get a new + /// record. Prmary keys and all required fields + /// must be filled in before insertion. + /// + public void Insert() + { + this.Insert(false); + } + + /// + /// Inserts the record into the table. + /// + /// true if the record is temporarily + /// inserted, to be visible only as long as the database is open + /// + /// The record (primary keys) may not already exist in the table. + /// Use to get a new + /// record. Prmary keys and all required fields + /// must be filled in before insertion. + /// + public void Insert(bool temporary) + { + using (Record updateRec = this.Database.CreateRecord(this.FieldCount)) + { + string insertSql = this.TableInfo.SqlInsertString; + if (temporary) + { + insertSql += " TEMPORARY"; + } + + TextWriter log = this.Database.Log; + if (log != null) + { + log.WriteLine(); + log.WriteLine(insertSql); + } + + for (int index = 0; index < this.FieldCount; index++) + { + ColumnInfo col = this.TableInfo.Columns[index]; + if (col.Type == typeof(Stream)) + { + throw new NotSupportedException( + "Cannot insert stream columns via QRecord."); + } + + if (this.Values[index] != null) + { + updateRec[index + 1] = this.Values[index]; + } + + if (log != null) + { + log.WriteLine(" ? = " + this.Values[index]); + } + } + + this.Database.Execute(insertSql, updateRec); + this.Exists = true; + } + } + + /// + /// Deletes the record from the table if it exists. + /// + public void Delete() + { + using (Record keyRec = this.Database.CreateRecord(this.TableInfo.PrimaryKeys.Count)) + { + StringBuilder s = new StringBuilder("DELETE FROM `"); + s.Append(this.TableInfo.Name); + s.Append("`"); + for (int i = 0; i < this.TableInfo.PrimaryKeys.Count; i++) + { + string key = this.TableInfo.PrimaryKeys[i]; + s.AppendFormat(" {0} `{1}` = ?", (i == 0 ? "WHERE" : "AND"), key); + int index = this.TableInfo.Columns.IndexOf(key); + keyRec[i + 1] = this.Values[index]; + } + + string deleteSql = s.ToString(); + + TextWriter log = this.Database.Log; + if (log != null) + { + log.WriteLine(); + log.WriteLine(deleteSql); + + for (int i = 0; i < this.TableInfo.PrimaryKeys.Count; i++) + { + log.WriteLine(" ? = " + keyRec.GetString(i + 1)); + } + } + + this.Database.Execute(deleteSql, keyRec); + this.Exists = false; + } + } + + /// + /// Not yet implemented. + /// + public void Refresh() + { + throw new NotImplementedException(); + } + + /// + /// Not yet implemented. + /// + public void Assign() + { + throw new NotImplementedException(); + } + + /// + /// Not yet implemented. + /// + public bool Merge() + { + throw new NotImplementedException(); + } + + /// + /// Not yet implemented. + /// + public ICollection Validate() + { + throw new NotImplementedException(); + } + + /// + /// Not yet implemented. + /// + [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")] + public ICollection ValidateNew() + { + throw new NotImplementedException(); + } + + /// + /// Not yet implemented. + /// + public ICollection ValidateFields() + { + throw new NotImplementedException(); + } + + /// + /// Not yet implemented. + /// + public ICollection ValidateDelete() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/QTable.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/QTable.cs new file mode 100644 index 000000000..eab1b0aa1 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/QTable.cs @@ -0,0 +1,302 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller.Linq +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Linq; + using System.Linq.Expressions; + + /// + /// Represents one table in a LINQ-queryable Database. + /// + /// type that represents one record in the table + /// + /// This class is the primary gateway to all LINQ to MSI query functionality. + /// The TRecord generic parameter may be the general + /// class, or a specialized subclass of QRecord. + /// + [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] + internal sealed class QTable : IOrderedQueryable, IQueryProvider + where TRecord : QRecord, new() + { + private QDatabase db; + private TableInfo tableInfo; + + /// + /// Infers the name of the table this instance will be + /// associated with. + /// + /// table name + /// + /// The table name is retrieved from a DatabaseTableAttribute + /// on the record type if it exists; otherwise the name is + /// derived from the name of the record type itself. + /// (An optional underscore suffix on the record type name is dropped.) + /// + private static string InferTableName() + { + foreach (DatabaseTableAttribute attr in typeof(TRecord).GetCustomAttributes( + typeof(DatabaseTableAttribute), false)) + { + string tableName = attr.Table; + if (!string.IsNullOrWhiteSpace(tableName)) + { + return tableName; + } + } + + string recordTypeName = typeof(TRecord).Name; + if (recordTypeName[recordTypeName.Length - 1] == '_') + { + return recordTypeName.Substring(0, recordTypeName.Length - 1); + } + else + { + return recordTypeName; + } + } + + /// + /// Creates a new QTable, inferring the table name + /// from the name of the record type parameter. + /// + /// database that contains the table + public QTable(QDatabase db) + : this(db, InferTableName()) + { + } + + /// + /// Creates a new QTable with an explicit table name. + /// + /// database that contains the table + /// name of the table + public QTable(QDatabase db, string table) + { + if (db == null) + { + throw new ArgumentNullException("db"); + } + + if (string.IsNullOrWhiteSpace(table)) + { + throw new ArgumentNullException("table"); + } + + this.db = db; + this.tableInfo = db.Tables[table]; + if (this.tableInfo == null) + { + throw new ArgumentException( + "Table does not exist in database: " + table); + } + } + + /// + /// Gets schema information about the table. + /// + public TableInfo TableInfo + { + get + { + return this.tableInfo; + } + } + + /// + /// Gets the database this table is associated with. + /// + public QDatabase Database + { + get + { + return this.db; + } + } + + /// + /// Enumerates over all records in the table. + /// + /// + public IEnumerator GetEnumerator() + { + string query = this.tableInfo.SqlSelectString; + + TextWriter log = this.db.Log; + if (log != null) + { + log.WriteLine(); + log.WriteLine(query); + } + + using (View view = db.OpenView(query)) + { + view.Execute(); + + ColumnCollection columns = this.tableInfo.Columns; + int columnCount = columns.Count; + bool[] isBinary = new bool[columnCount]; + + for (int i = 0; i < isBinary.Length; i++) + { + isBinary[i] = columns[i].Type == typeof(System.IO.Stream); + } + + foreach (Record rec in view) using (rec) + { + string[] values = new string[columnCount]; + for (int i = 0; i < values.Length; i++) + { + values[i] = isBinary[i] ? "[Binary Data]" : rec.GetString(i + 1); + } + + TRecord trec = new TRecord(); + trec.Database = this.Database; + trec.TableInfo = this.TableInfo; + trec.Values = values; + trec.Exists = true; + yield return trec; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable) this).GetEnumerator(); + } + + IQueryable IQueryProvider.CreateQuery(Expression expression) + { + if (expression == null) + { + throw new ArgumentNullException("expression"); + } + + Query q = new Query(this.Database, expression); + + MethodCallExpression methodCallExpression = (MethodCallExpression) expression; + string methodName = methodCallExpression.Method.Name; + if (methodName == "Where") + { + LambdaExpression argumentExpression = (LambdaExpression) + ((UnaryExpression) methodCallExpression.Arguments[1]).Operand; + q.BuildQuery(this.TableInfo, argumentExpression); + } + else if (methodName == "OrderBy") + { + LambdaExpression argumentExpression = (LambdaExpression) + ((UnaryExpression) methodCallExpression.Arguments[1]).Operand; + q.BuildSequence(this.TableInfo, argumentExpression); + } + else if (methodName == "Select") + { + LambdaExpression argumentExpression = (LambdaExpression) + ((UnaryExpression) methodCallExpression.Arguments[1]).Operand; + q.BuildNullQuery(this.TableInfo, typeof(TRecord), argumentExpression); + q.BuildProjection(null, argumentExpression); + } + else if (methodName == "Join") + { + ConstantExpression constantExpression = (ConstantExpression) + methodCallExpression.Arguments[1]; + IQueryable inner = (IQueryable) constantExpression.Value; + q.PerformJoin( + this.TableInfo, + typeof(TRecord), + inner, + GetJoinLambda(methodCallExpression.Arguments[2]), + GetJoinLambda(methodCallExpression.Arguments[3]), + GetJoinLambda(methodCallExpression.Arguments[4])); + } + else + { + throw new NotSupportedException( + "Query operation not supported: " + methodName); + } + + return q; + } + + private static LambdaExpression GetJoinLambda(Expression expresion) + { + UnaryExpression unaryExpression = (UnaryExpression) expresion; + return (LambdaExpression) unaryExpression.Operand; + } + + IQueryable IQueryProvider.CreateQuery(Expression expression) + { + return ((IQueryProvider) this).CreateQuery(expression); + } + + TResult IQueryProvider.Execute(Expression expression) + { + throw new NotSupportedException( + "Direct method calls not supported -- use AsEnumerable() instead."); + } + + object IQueryProvider.Execute(Expression expression) + { + throw new NotSupportedException( + "Direct method calls not supported -- use AsEnumerable() instead."); + } + + IQueryProvider IQueryable.Provider + { + get + { + return this; + } + } + + Type IQueryable.ElementType + { + get + { + return typeof(TRecord); + } + } + + Expression IQueryable.Expression + { + get + { + return Expression.Constant(this); + } + } + + /// + /// Creates a new record that can be inserted into this table. + /// + /// a record with all fields initialized to null + /// + /// Primary keys and required fields must be filled in with + /// non-null values before the record can be inserted. + /// The record is tied to this table in this database; + /// it cannot be inserted into another table or database. + /// + public TRecord NewRecord() + { + TRecord rec = new TRecord(); + rec.Database = this.Database; + rec.TableInfo = this.TableInfo; + IList values = new List(this.TableInfo.Columns.Count); + for (int i = 0; i < this.TableInfo.Columns.Count; i++) + { + values.Add(null); + } + rec.Values = values; + return rec; + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/Query.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/Query.cs new file mode 100644 index 000000000..189228972 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Linq/Query.cs @@ -0,0 +1,999 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller.Linq +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + using System.Text; + + /// + /// Implements the LINQ to MSI query functionality. + /// + /// the result type of the current query -- + /// either some kind of QRecord, or some projection of record data + internal sealed class Query : IOrderedQueryable, IQueryProvider + { + private QDatabase db; + private Expression queryableExpression; + private List tables; + private List recordTypes; + private List selectors; + private string where; + private List whereParameters; + private List orderbyColumns; + private List selectColumns; + private List joinColumns; + private List projectionDelegates; + + internal Query(QDatabase db, Expression expression) + { + if (expression == null) + { + throw new ArgumentNullException("expression"); + } + + this.db = db; + this.queryableExpression = expression; + this.tables = new List(); + this.recordTypes = new List(); + this.selectors = new List(); + this.whereParameters = new List(); + this.orderbyColumns = new List(); + this.selectColumns = new List(); + this.joinColumns = new List(); + this.projectionDelegates = new List(); + } + + public IEnumerator GetEnumerator() + { + if (this.selectColumns.Count == 0) + { + AddAllColumns(this.tables[0], this.selectColumns); + } + + string query = this.CompileQuery(); + return this.InvokeQuery(query); + } + + private string CompileQuery() + { + bool explicitTables = this.tables.Count > 1; + + StringBuilder queryBuilder = new StringBuilder("SELECT"); + + for (int i = 0; i < this.selectColumns.Count; i++) + { + queryBuilder.AppendFormat( + CultureInfo.InvariantCulture, + (explicitTables ? "{0} `{1}`.`{2}`" : "{0} `{2}`"), + (i > 0 ? "," : String.Empty), + this.selectColumns[i].Table.Name, + this.selectColumns[i].Column.Name); + } + + for (int i = 0; i < this.tables.Count; i++) + { + queryBuilder.AppendFormat( + CultureInfo.InvariantCulture, + "{0} `{1}`", + (i == 0 ? " FROM" : ","), + this.tables[i].Name); + } + + bool startedWhere = false; + for (int i = 0; i < this.joinColumns.Count - 1; i += 2) + { + queryBuilder.AppendFormat( + CultureInfo.InvariantCulture, + "{0} `{1}`.`{2}` = `{3}`.`{4}` ", + (i == 0 ? " WHERE" : "AND"), + this.joinColumns[i].Table, + this.joinColumns[i].Column, + this.joinColumns[i + 1].Table, + this.joinColumns[i + 1].Column); + startedWhere = true; + } + + if (this.where != null) + { + queryBuilder.Append(startedWhere ? "AND " : " WHERE"); + queryBuilder.Append(this.where); + } + + for (int i = 0; i < this.orderbyColumns.Count; i++) + { + VerifyOrderByColumn(this.orderbyColumns[i]); + + queryBuilder.AppendFormat( + CultureInfo.InvariantCulture, + (explicitTables ? "{0} `{1}`.`{2}`" : "{0} `{2}`"), + (i == 0 ? " ORDER BY" : ","), + this.orderbyColumns[i].Table.Name, + this.orderbyColumns[i].Column.Name); + } + + return queryBuilder.ToString(); + } + + private static void VerifyOrderByColumn(TableColumn tableColumn) + { + if (tableColumn.Column.Type != typeof(int) && + tableColumn.Column.Type != typeof(short)) + { + throw new NotSupportedException( + "Cannot orderby column: " + tableColumn.Column.Name + + "; orderby is only supported on integer fields"); + } + } + + private IEnumerator InvokeQuery(string query) + { + TextWriter log = this.db.Log; + if (log != null) + { + log.WriteLine(); + log.WriteLine(query); + } + + using (View queryView = this.db.OpenView(query)) + { + if (this.whereParameters != null && this.whereParameters.Count > 0) + { + using (Record paramsRec = this.db.CreateRecord(this.whereParameters.Count)) + { + for (int i = 0; i < this.whereParameters.Count; i++) + { + paramsRec[i + 1] = this.whereParameters[i]; + + if (log != null) + { + log.WriteLine(" ? = " + this.whereParameters[i]); + } + } + + queryView.Execute(paramsRec); + } + } + else + { + queryView.Execute(); + } + + foreach (Record resultRec in queryView) using (resultRec) + { + yield return this.GetResult(resultRec); + } + } + } + + private T GetResult(Record resultRec) + { + object[] results = new object[this.tables.Count]; + + for (int i = 0; i < this.tables.Count; i++) + { + string[] values = new string[this.tables[i].Columns.Count]; + for (int j = 0; j < this.selectColumns.Count; j++) + { + TableColumn col = this.selectColumns[j]; + if (col.Table.Name == this.tables[i].Name) + { + int index = this.tables[i].Columns.IndexOf( + col.Column.Name); + if (index >= 0) + { + if (col.Column.Type == typeof(Stream)) + { + values[index] = "[Binary Data]"; + } + else + { + values[index] = resultRec.GetString(j + 1); + } + } + } + } + + QRecord result = (QRecord) this.recordTypes[i] + .GetConstructor(Type.EmptyTypes).Invoke(null); + result.Database = this.db; + result.TableInfo = this.tables[i]; + result.Values = values; + result.Exists = true; + results[i] = result; + } + + if (this.projectionDelegates.Count > 0) + { + object resultsProjection = results[0]; + for (int i = 1; i <= results.Length; i++) + { + if (i < results.Length) + { + resultsProjection = this.projectionDelegates[i - 1] + .DynamicInvoke(new object[] { resultsProjection, results[i] }); + } + else + { + resultsProjection = this.projectionDelegates[i - 1] + .DynamicInvoke(resultsProjection); + } + } + + return (T) resultsProjection; + } + else + { + return (T) (object) results[0]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable) this).GetEnumerator(); + } + + public IQueryable CreateQuery(Expression expression) + { + if (expression == null) + { + throw new ArgumentNullException("expression"); + } + + Query q = new Query(this.db, expression); + q.tables.AddRange(this.tables); + q.recordTypes.AddRange(this.recordTypes); + q.selectors.AddRange(this.selectors); + q.where = this.where; + q.whereParameters.AddRange(this.whereParameters); + q.orderbyColumns.AddRange(this.orderbyColumns); + q.selectColumns.AddRange(this.selectColumns); + q.joinColumns.AddRange(this.joinColumns); + q.projectionDelegates.AddRange(this.projectionDelegates); + + MethodCallExpression methodCallExpression = (MethodCallExpression) expression; + string methodName = methodCallExpression.Method.Name; + if (methodName == "Select") + { + LambdaExpression argumentExpression = (LambdaExpression) + ((UnaryExpression) methodCallExpression.Arguments[1]).Operand; + q.BuildProjection(null, argumentExpression); + } + else if (methodName == "Where") + { + LambdaExpression argumentExpression = (LambdaExpression) + ((UnaryExpression) methodCallExpression.Arguments[1]).Operand; + q.BuildQuery(null, argumentExpression); + } + else if (methodName == "ThenBy") + { + LambdaExpression argumentExpression = (LambdaExpression) + ((UnaryExpression) methodCallExpression.Arguments[1]).Operand; + q.BuildSequence(null, argumentExpression); + } + else if (methodName == "Join") + { + ConstantExpression constantExpression = (ConstantExpression) + methodCallExpression.Arguments[1]; + IQueryable inner = (IQueryable) constantExpression.Value; + q.PerformJoin( + null, + null, + inner, + GetJoinLambda(methodCallExpression.Arguments[2]), + GetJoinLambda(methodCallExpression.Arguments[3]), + GetJoinLambda(methodCallExpression.Arguments[4])); + } + else + { + throw new NotSupportedException( + "Query operation not supported: " + methodName); + } + + return q; + } + + public IQueryable CreateQuery(Expression expression) + { + return this.CreateQuery(expression); + } + + private static LambdaExpression GetJoinLambda(Expression expresion) + { + UnaryExpression unaryExpression = (UnaryExpression) expresion; + return (LambdaExpression) unaryExpression.Operand; + } + + public TResult Execute(Expression expression) + { + throw new NotSupportedException( + "Direct method calls not supported -- use AsEnumerable() instead."); + } + + object IQueryProvider.Execute(Expression expression) + { + throw new NotSupportedException( + "Direct method calls not supported -- use AsEnumerable() instead."); + } + + public IQueryProvider Provider + { + get + { + return this; + } + } + + public Type ElementType + { + get + { + return typeof(T); + } + } + + public Expression Expression + { + get + { + return this.queryableExpression; + } + } + + internal void BuildQuery(TableInfo tableInfo, LambdaExpression expression) + { + if (tableInfo != null) + { + this.tables.Add(tableInfo); + this.recordTypes.Add(typeof(T)); + this.selectors.Add(expression.Parameters[0].Name); + } + + StringBuilder queryBuilder = new StringBuilder(); + + this.ParseQuery(expression.Body, queryBuilder); + + this.where = queryBuilder.ToString(); + } + + internal void BuildNullQuery(TableInfo tableInfo, Type recordType, LambdaExpression expression) + { + this.tables.Add(tableInfo); + this.recordTypes.Add(recordType); + this.selectors.Add(expression.Parameters[0].Name); + } + + private void ParseQuery(Expression expression, StringBuilder queryBuilder) + { + queryBuilder.Append("("); + + BinaryExpression binaryExpression; + UnaryExpression unaryExpression; + MethodCallExpression methodCallExpression; + + if ((binaryExpression = expression as BinaryExpression) != null) + { + switch (binaryExpression.NodeType) + { + case ExpressionType.AndAlso: + this.ParseQuery(binaryExpression.Left, queryBuilder); + queryBuilder.Append(" AND "); + this.ParseQuery(binaryExpression.Right, queryBuilder); + break; + + case ExpressionType.OrElse: + this.ParseQuery(binaryExpression.Left, queryBuilder); + queryBuilder.Append(" OR "); + this.ParseQuery(binaryExpression.Right, queryBuilder); + break; + + case ExpressionType.Equal: + case ExpressionType.NotEqual: + case ExpressionType.GreaterThan: + case ExpressionType.LessThan: + case ExpressionType.GreaterThanOrEqual: + case ExpressionType.LessThanOrEqual: + this.ParseQueryCondition(binaryExpression, queryBuilder); + break; + + default: + throw new NotSupportedException( + "Expression type not supported: " + binaryExpression.NodeType ); + } + } + else if ((unaryExpression = expression as UnaryExpression) != null) + { + throw new NotSupportedException( + "Expression type not supported: " + unaryExpression.NodeType); + } + else if ((methodCallExpression = expression as MethodCallExpression) != null) + { + throw new NotSupportedException( + "Method call not supported: " + methodCallExpression.Method.Name + "()"); + } + else + { + throw new NotSupportedException( + "Query filter expression not supported: " + expression); + } + + queryBuilder.Append(")"); + } + + private static ExpressionType OppositeExpression(ExpressionType e) + { + switch (e) + { + case ExpressionType.LessThan: + return ExpressionType.GreaterThan; + case ExpressionType.LessThanOrEqual: + return ExpressionType.GreaterThanOrEqual; + case ExpressionType.GreaterThan: + return ExpressionType.LessThan; + case ExpressionType.GreaterThanOrEqual: + return ExpressionType.LessThanOrEqual; + default: + return e; + } + } + + private static bool IsIntegerType(Type t) + { + return + t == typeof(sbyte) || + t == typeof(byte) || + t == typeof(short) || + t == typeof(ushort) || + t == typeof(int) || + t == typeof(uint) || + t == typeof(long) || + t == typeof(ulong); + } + + private void ParseQueryCondition( + BinaryExpression binaryExpression, StringBuilder queryBuilder) + { + bool swap; + string column = this.GetConditionColumn(binaryExpression, out swap); + queryBuilder.Append(column); + + ExpressionType expressionType = binaryExpression.NodeType; + if (swap) + { + expressionType = OppositeExpression(expressionType); + } + + LambdaExpression valueExpression = Expression.Lambda( + swap ? binaryExpression.Left : binaryExpression.Right); + object value = valueExpression.Compile().DynamicInvoke(); + + bool valueIsInt = false; + if (value != null) + { + if (IsIntegerType(value.GetType())) + { + valueIsInt = true; + } + else + { + value = value.ToString(); + } + } + + switch (expressionType) + { + case ExpressionType.Equal: + if (value == null) + { + queryBuilder.Append(" IS NULL"); + } + else if (valueIsInt) + { + queryBuilder.Append(" = "); + queryBuilder.Append(value); + } + else + { + queryBuilder.Append(" = ?"); + this.whereParameters.Add(value); + } + return; + + case ExpressionType.NotEqual: + if (value == null) + { + queryBuilder.Append(" IS NOT NULL"); + } + else if (valueIsInt) + { + queryBuilder.Append(" <> "); + queryBuilder.Append(value); + } + else + { + queryBuilder.Append(" <> ?"); + this.whereParameters.Add(value); + } + return; + } + + if (value == null) + { + throw new InvalidOperationException( + "A null value was used in a greater-than/less-than operation."); + } + + if (!valueIsInt) + { + throw new NotSupportedException( + "Greater-than/less-than operators not supported on strings."); + } + + switch (expressionType) + { + case ExpressionType.LessThan: + queryBuilder.Append(" < "); + break; + + case ExpressionType.LessThanOrEqual: + queryBuilder.Append(" <= "); + break; + + case ExpressionType.GreaterThan: + queryBuilder.Append(" > "); + break; + + case ExpressionType.GreaterThanOrEqual: + queryBuilder.Append(" >= "); + break; + + default: + throw new NotSupportedException( + "Unsupported query expression type: " + expressionType); + } + + queryBuilder.Append(value); + } + + private string GetConditionColumn( + BinaryExpression binaryExpression, out bool swap) + { + MemberExpression memberExpression; + MethodCallExpression methodCallExpression; + + if (((memberExpression = binaryExpression.Left as MemberExpression) != null) || + ((binaryExpression.Left.NodeType == ExpressionType.Convert || + binaryExpression.Left.NodeType == ExpressionType.ConvertChecked) && + (memberExpression = ((UnaryExpression) binaryExpression.Left).Operand + as MemberExpression) != null)) + { + string column = this.GetConditionColumn(memberExpression); + if (column != null) + { + swap = false; + return column; + } + } + else if (((memberExpression = binaryExpression.Right as MemberExpression) != null) || + ((binaryExpression.Right.NodeType == ExpressionType.Convert || + binaryExpression.Right.NodeType == ExpressionType.ConvertChecked) && + (memberExpression = ((UnaryExpression) binaryExpression.Right).Operand + as MemberExpression) != null)) + { + string column = this.GetConditionColumn(memberExpression); + if (column != null) + { + swap = true; + return column; + } + } + else if ((methodCallExpression = binaryExpression.Left as MethodCallExpression) != null) + { + string column = this.GetConditionColumn(methodCallExpression); + if (column != null) + { + swap = false; + return column; + } + } + else if ((methodCallExpression = binaryExpression.Right as MethodCallExpression) != null) + { + string column = this.GetConditionColumn(methodCallExpression); + if (column != null) + { + swap = true; + return column; + } + } + + throw new NotSupportedException( + "Unsupported binary expression: " + binaryExpression); + } + + private string GetConditionColumn(MemberExpression memberExpression) + { + string columnName = GetColumnName(memberExpression.Member); + string selectorName = GetConditionSelectorName(memberExpression.Expression); + string tableName = this.GetConditionTable(selectorName, columnName); + return this.FormatColumn(tableName, columnName); + } + + private string GetConditionColumn(MethodCallExpression methodCallExpression) + { + LambdaExpression argumentExpression = + Expression.Lambda(methodCallExpression.Arguments[0]); + string columnName = (string) argumentExpression.Compile().DynamicInvoke(); + string selectorName = GetConditionSelectorName(methodCallExpression.Object); + string tableName = this.GetConditionTable(selectorName, columnName); + return this.FormatColumn(tableName, columnName); + } + + private static string GetConditionSelectorName(Expression expression) + { + ParameterExpression parameterExpression; + MemberExpression memberExpression; + if ((parameterExpression = expression as ParameterExpression) != null) + { + return parameterExpression.Name; + } + else if ((memberExpression = expression as MemberExpression) != null) + { + return memberExpression.Member.Name; + } + else + { + throw new NotSupportedException( + "Unsupported conditional selector expression: " + expression); + } + } + + private string GetConditionTable(string selectorName, string columnName) + { + string tableName = null; + + for (int i = 0; i < this.tables.Count; i++) + { + if (this.selectors[i] == selectorName) + { + tableName = this.tables[i].Name; + break; + } + } + + if (tableName == null) + { + throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, + "Conditional expression contains column {0}.{1} " + + "from a table that is not in the query.", + selectorName, + columnName)); + } + + return tableName; + } + + private string FormatColumn(string tableName, string columnName) + { + if (tableName != null && this.tables.Count > 1) + { + return String.Format(CultureInfo.InvariantCulture, "`{0}`.`{1}`", tableName, columnName); + } + else + { + return String.Format(CultureInfo.InvariantCulture, "`{0}`", columnName); + } + } + + private static string GetColumnName(MemberInfo memberInfo) + { + foreach (var attr in memberInfo.GetCustomAttributes( + typeof(DatabaseColumnAttribute), false)) + { + return ((DatabaseColumnAttribute) attr).Column; + } + + return memberInfo.Name; + } + + internal void BuildProjection(TableInfo tableInfo, LambdaExpression expression) + { + if (tableInfo != null) + { + this.tables.Add(tableInfo); + this.recordTypes.Add(typeof(T)); + this.selectors.Add(expression.Parameters[0].Name); + } + + this.FindColumns(expression, this.selectColumns); + this.projectionDelegates.Add(expression.Compile()); + } + + internal void BuildSequence(TableInfo tableInfo, LambdaExpression expression) + { + if (tableInfo != null) + { + this.tables.Add(tableInfo); + this.recordTypes.Add(typeof(T)); + this.selectors.Add(expression.Parameters[0].Name); + } + + this.FindColumns(expression.Body, this.orderbyColumns); + } + + private static void AddAllColumns(TableInfo tableInfo, IList columnList) + { + foreach (ColumnInfo column in tableInfo.Columns) + { + columnList.Add(new TableColumn(tableInfo, column)); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")] + private void FindColumns(Expression expression, IList columnList) + { + if (expression is ParameterExpression) + { + ParameterExpression e = expression as ParameterExpression; + string selector = e.Name; + for (int i = 0; i < this.tables.Count; i++) + { + if (this.selectors[i] == selector) + { + AddAllColumns(this.tables[i], columnList); + break; + } + } + } + else if (expression.NodeType == ExpressionType.MemberAccess) + { + this.FindColumns(expression as MemberExpression, columnList); + } + else if (expression is MethodCallExpression) + { + this.FindColumns(expression as MethodCallExpression, columnList); + } + else if (expression is BinaryExpression) + { + BinaryExpression e = expression as BinaryExpression; + this.FindColumns(e.Left, columnList); + this.FindColumns(e.Right, columnList); + } + else if (expression is UnaryExpression) + { + UnaryExpression e = expression as UnaryExpression; + this.FindColumns(e.Operand, columnList); + } + else if (expression is ConditionalExpression) + { + ConditionalExpression e = expression as ConditionalExpression; + this.FindColumns(e.Test, columnList); + this.FindColumns(e.IfTrue, columnList); + this.FindColumns(e.IfFalse, columnList); + } + else if (expression is InvocationExpression) + { + InvocationExpression e = expression as InvocationExpression; + this.FindColumns(e.Expression, columnList); + this.FindColumns(e.Arguments, columnList); + } + else if (expression is LambdaExpression) + { + LambdaExpression e = expression as LambdaExpression; + this.FindColumns(e.Body, columnList); + } + else if (expression is ListInitExpression) + { + ListInitExpression e = expression as ListInitExpression; + this.FindColumns(e.NewExpression, columnList); + foreach (ElementInit ei in e.Initializers) + { + this.FindColumns(ei.Arguments, columnList); + } + } + else if (expression is MemberInitExpression) + { + MemberInitExpression e = expression as MemberInitExpression; + this.FindColumns(e.NewExpression, columnList); + foreach (MemberAssignment b in e.Bindings) + { + this.FindColumns(b.Expression, columnList); + } + } + else if (expression is NewExpression) + { + NewExpression e = expression as NewExpression; + this.FindColumns(e.Arguments, columnList); + } + else if (expression is NewArrayExpression) + { + NewArrayExpression e = expression as NewArrayExpression; + this.FindColumns(e.Expressions, columnList); + } + else if (expression is TypeBinaryExpression) + { + TypeBinaryExpression e = expression as TypeBinaryExpression; + this.FindColumns(e.Expression, columnList); + } + } + + private void FindColumns(IEnumerable expressions, IList columnList) + { + foreach (Expression expression in expressions) + { + this.FindColumns(expression, columnList); + } + } + + private void FindColumns(MemberExpression memberExpression, IList columnList) + { + string selector = null; + MemberExpression objectMemberExpression; + ParameterExpression objectParameterExpression; + if ((objectParameterExpression = memberExpression.Expression as + ParameterExpression) != null) + { + selector = objectParameterExpression.Name; + } + else if ((objectMemberExpression = memberExpression.Expression as + MemberExpression) != null) + { + selector = objectMemberExpression.Member.Name; + } + + if (selector != null) + { + for (int i = 0; i < this.tables.Count; i++) + { + if (this.selectors[i] == selector) + { + string columnName = GetColumnName(memberExpression.Member); + ColumnInfo column = this.tables[i].Columns[columnName]; + columnList.Add(new TableColumn(this.tables[i], column)); + break; + } + } + } + + selector = memberExpression.Member.Name; + for (int i = 0; i < this.tables.Count; i++) + { + if (this.selectors[i] == selector) + { + AddAllColumns(this.tables[i], columnList); + break; + } + } + } + + private void FindColumns(MethodCallExpression methodCallExpression, IList columnList) + { + if (methodCallExpression.Method.Name == "get_Item" && + methodCallExpression.Arguments.Count == 1 && + methodCallExpression.Arguments[0].Type == typeof(string)) + { + string selector = null; + MemberExpression objectMemberExpression; + ParameterExpression objectParameterExpression; + if ((objectParameterExpression = methodCallExpression.Object as ParameterExpression) != null) + { + selector = objectParameterExpression.Name; + } + else if ((objectMemberExpression = methodCallExpression.Object as MemberExpression) != null) + { + selector = objectMemberExpression.Member.Name; + } + + if (selector != null) + { + for (int i = 0; i < this.tables.Count; i++) + { + if (this.selectors[i] == selector) + { + LambdaExpression argumentExpression = + Expression.Lambda(methodCallExpression.Arguments[0]); + string columnName = (string) + argumentExpression.Compile().DynamicInvoke(); + ColumnInfo column = this.tables[i].Columns[columnName]; + columnList.Add(new TableColumn(this.tables[i], column)); + break; + } + } + } + } + + if (methodCallExpression.Object != null && methodCallExpression.Object.NodeType != ExpressionType.Parameter) + { + this.FindColumns(methodCallExpression.Object, columnList); + } + } + + internal void PerformJoin( + TableInfo tableInfo, + Type recordType, + IQueryable joinTable, + LambdaExpression outerKeySelector, + LambdaExpression innerKeySelector, + LambdaExpression resultSelector) + { + if (joinTable == null) + { + throw new ArgumentNullException("joinTable"); + } + + if (tableInfo != null) + { + this.tables.Add(tableInfo); + this.recordTypes.Add(recordType); + this.selectors.Add(outerKeySelector.Parameters[0].Name); + } + + PropertyInfo tableInfoProp = joinTable.GetType().GetProperty("TableInfo"); + if (tableInfoProp == null) + { + throw new NotSupportedException( + "Cannot join with object: " + joinTable.GetType().Name + + "; join is only supported on another QTable."); + } + + TableInfo joinTableInfo = (TableInfo) tableInfoProp.GetValue(joinTable, null); + if (joinTableInfo == null) + { + throw new InvalidOperationException("Missing join table info."); + } + + this.tables.Add(joinTableInfo); + this.recordTypes.Add(joinTable.ElementType); + this.selectors.Add(innerKeySelector.Parameters[0].Name); + this.projectionDelegates.Add(resultSelector.Compile()); + + int joinColumnCount = this.joinColumns.Count; + this.FindColumns(outerKeySelector.Body, this.joinColumns); + if (this.joinColumns.Count > joinColumnCount + 1) + { + throw new NotSupportedException("Join operations involving " + + "multiple columns are not supported."); + } + else if (this.joinColumns.Count != joinColumnCount + 1) + { + throw new InvalidOperationException("Bad outer key selector for join."); + } + + this.FindColumns(innerKeySelector.Body, this.joinColumns); + if (this.joinColumns.Count > joinColumnCount + 2) + { + throw new NotSupportedException("Join operations involving " + + "multiple columns not are supported."); + } + if (this.joinColumns.Count != joinColumnCount + 2) + { + throw new InvalidOperationException("Bad inner key selector for join."); + } + } + } + + internal class TableColumn + { + public TableColumn(TableInfo table, ColumnInfo column) + { + this.Table = table; + this.Column = column; + } + + public TableInfo Table { get; set; } + public ColumnInfo Column { get; set; } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Package/InstallPackage.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Package/InstallPackage.cs new file mode 100644 index 000000000..a66cc9995 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Package/InstallPackage.cs @@ -0,0 +1,1202 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller.Package +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Text.RegularExpressions; + using Archivers.Internal.Compression; + using Archivers.Internal.Compression.Cab; + using FileAttributes = WindowsInstaller.FileAttributes; + + /// +/// Handles status messages generated when operations are performed on an +/// or . +/// +/// +/// installPackage.Message += new InstallPackageMessageHandler(Console.WriteLine); +/// +public delegate void InstallPackageMessageHandler(string format, params object[] args); + +/// +/// Provides access to powerful build, maintenance, and analysis operations on an +/// installation package (.MSI or .MSM). +/// +internal class InstallPackage : Database +{ + private string cabName; + private string cabMsg; + + /// + /// Creates a new InstallPackage object. The file source directory and working + /// directory are the same as the location as the package file. + /// + /// Path to the install package to be created or opened + /// Open mode for the database + public InstallPackage(string packagePath, DatabaseOpenMode openMode) + : this(packagePath, openMode, null, null) + { + } + /// + /// Creates a new InstallPackage object, specifying an alternate file source + /// directory and/or working directory. + /// + /// Path to the install package to be created or opened + /// Open mode for the database + /// Location to obtain source files and cabinets when extracting + /// or updating files in the working directory. This is often the location of an original + /// copy of the package that is not meant to be modified. If this parameter is null, it + /// defaults to the directory of . + /// Location where files will be extracted to/updated from. Also + /// the location where a temporary folder is created during some operations. If this + /// parameter is null, it defaults to the directory of . + /// If the source location is different than the working directory, then + /// no files will be modified at the source location. + /// + public InstallPackage(string packagePath, DatabaseOpenMode openMode, + string sourceDir, string workingDir) : base(packagePath, openMode) + { + this.sourceDir = (sourceDir != null ? sourceDir : Path.GetDirectoryName(packagePath)); + this.workingDir = (workingDir != null ? workingDir : Path.GetDirectoryName(packagePath)); + this.compressionLevel = CompressionLevel.Normal; + + this.DeleteOnClose(this.TempDirectory); + } + + /// + /// Handle this event to receive status messages when operations are performed + /// on the install package. + /// + /// + /// installPackage.Message += new InstallPackageMessageHandler(Console.WriteLine); + /// + public event InstallPackageMessageHandler Message; + + /// + /// Sends a message to the event-handler. + /// + /// Message string, containing 0 or more format items + /// Items to be formatted + protected void LogMessage(string format, params object[] args) + { + if(this.Message != null) + { + this.Message(format, args); + } + } + + /// + /// Gets or sets the location to obtain source files and cabinets when + /// extracting or updating files in the working directory. This is often + /// the location of an original copy of the package that is not meant + /// to be modified. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string SourceDirectory + { + get { return this.sourceDir; } + set { this.sourceDir = value; } + } + private string sourceDir; + + /// + /// Gets or sets the location where files will be extracted to/updated from. Also + /// the location where a temporary folder is created during some operations. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string WorkingDirectory + { + get { return this.workingDir; } + set { this.workingDir = value; } + } + private string workingDir; + + private const string TEMP_DIR_NAME = "WITEMP"; + + private string TempDirectory + { + get { return Path.Combine(this.WorkingDirectory, TEMP_DIR_NAME); } + } + + /// + /// Gets the list of file keys that have the specified long file name. + /// + /// File name to search for (case-insensitive) + /// Array of file keys, or a 0-length array if none are found + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string[] FindFiles(string longFileName) + { + if (longFileName == null) { + throw new ArgumentNullException("longFileName"); + } + longFileName = longFileName.ToLowerInvariant(); + ArrayList fileList = new ArrayList(); + foreach(KeyValuePair entry in this.Files) + { + if(((InstallPath) entry.Value).TargetName.ToLowerInvariant() + == longFileName) + { + fileList.Add(entry.Key); + } + } + return (string[]) fileList.ToArray(typeof(string)); + } + + /// + /// Gets the list of file keys whose long file names match a specified + /// regular-expression search pattern. + /// + /// Regular expression search pattern + /// Array of file keys, or a 0-length array if none are found + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string[] FindFiles(Regex pattern) + { + if (pattern== null) { + throw new ArgumentNullException("pattern"); + } + ArrayList fileList = new ArrayList(); + foreach (KeyValuePair entry in this.Files) + { + if(pattern.IsMatch(((InstallPath) entry.Value).TargetName)) + { + fileList.Add(entry.Key); + } + } + return (string[]) fileList.ToArray(typeof(string)); + } + + /// + /// Extracts all files to the . The files are extracted + /// to the relative directory matching their . + /// + /// If any files have the uncompressed attribute, they will be copied + /// from the . + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void ExtractFiles() + { + this.ExtractFiles(null); + } + /// + /// Extracts a specified list of files to the . The files + /// are extracted to the relative directory matching their . + /// + /// List of file key strings to extract + /// If any files have the uncompressed attribute, they will be copied + /// from the . + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void ExtractFiles(ICollection fileKeys) + { + this.ProcessFilesByMediaDisk(fileKeys, + new ProcessFilesOnOneMediaDiskHandler(this.ExtractFilesOnOneMediaDisk)); + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + private bool IsMergeModule() + { + return this.CountRows("Media", "`LastSequence` >= 0") == 0 && + this.CountRows("_Streams", "`Name` = 'MergeModule.CABinet'") != 0; + } + + private delegate void ProcessFilesOnOneMediaDiskHandler(string mediaCab, + InstallPathMap compressedFileMap, InstallPathMap uncompressedFileMap); + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + private void ProcessFilesByMediaDisk(ICollection fileKeys, + ProcessFilesOnOneMediaDiskHandler diskHandler) + { + if(this.IsMergeModule()) + { + InstallPathMap files = new InstallPathMap(); + foreach(string fileKey in this.Files.Keys) + { + if(fileKeys == null || fileKeys.Contains(fileKey)) + { + files[fileKey] = this.Files[fileKey]; + } + } + diskHandler("#MergeModule.CABinet", files, new InstallPathMap()); + } + else + { + bool defaultCompressed = ((this.SummaryInfo.WordCount & 0x2) != 0); + + View fileView = null, mediaView = null; + Record fileRec = null; + try + { + fileView = this.OpenView("SELECT `File`, `Attributes`, `Sequence` " + + "FROM `File` ORDER BY `Sequence`"); + mediaView = this.OpenView("SELECT `DiskId`, `LastSequence`, `Cabinet` " + + "FROM `Media` ORDER BY `DiskId`"); + fileView.Execute(); + mediaView.Execute(); + + int currentMediaDiskId = -1; + int currentMediaMaxSequence = -1; + string currentMediaCab = null; + InstallPathMap compressedFileMap = new InstallPathMap(); + InstallPathMap uncompressedFileMap = new InstallPathMap(); + + while((fileRec = fileView.Fetch()) != null) + { + string fileKey = (string) fileRec[1]; + + if(fileKeys == null || fileKeys.Contains(fileKey)) + { + int fileAttributes = fileRec.GetInteger(2); + int fileSequence = fileRec.GetInteger(3); + + InstallPath fileInstallPath = this.Files[fileKey]; + if(fileInstallPath == null) + { + this.LogMessage("Could not get install path for source file: {0}", fileKey); + throw new InstallerException("Could not get install path for source file: " + fileKey); + } + + if(fileSequence > currentMediaMaxSequence) + { + if(currentMediaDiskId != -1) + { + diskHandler(currentMediaCab, + compressedFileMap, uncompressedFileMap); + compressedFileMap.Clear(); + uncompressedFileMap.Clear(); + } + + while(fileSequence > currentMediaMaxSequence) + { + Record mediaRec = mediaView.Fetch(); + if(mediaRec == null) + { + currentMediaDiskId = -1; + break; + } + using(mediaRec) + { + currentMediaDiskId = mediaRec.GetInteger(1); + currentMediaMaxSequence = mediaRec.GetInteger(2); + currentMediaCab = (string) mediaRec[3]; + } + } + if(fileSequence > currentMediaMaxSequence) break; + } + + if((fileAttributes & (int) FileAttributes.Compressed) != 0) + { + compressedFileMap[fileKey] = fileInstallPath; + } + else if ((fileAttributes & (int) FileAttributes.NonCompressed) != 0) + { + // Non-compressed files are located + // in the same directory as the MSI, without any path. + uncompressedFileMap[fileKey] = new InstallPath(fileInstallPath.SourceName); + } + else if(defaultCompressed) + { + compressedFileMap[fileKey] = fileInstallPath; + } + else + { + uncompressedFileMap[fileKey] = fileInstallPath; + } + } + fileRec.Close(); + fileRec = null; + } + if(currentMediaDiskId != -1) + { + diskHandler(currentMediaCab, + compressedFileMap, uncompressedFileMap); + } + } + finally + { + if (fileRec != null) fileRec.Close(); + if (fileView != null) fileView.Close(); + if (mediaView != null) mediaView.Close(); + } + } + } + + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")] + private void ExtractFilesOnOneMediaDisk(string mediaCab, + InstallPathMap compressedFileMap, InstallPathMap uncompressedFileMap) + { + if(compressedFileMap.Count > 0) + { + string cabFile = null; + if(mediaCab.StartsWith("#", StringComparison.Ordinal)) + { + mediaCab = mediaCab.Substring(1); + + using(View streamView = this.OpenView("SELECT `Name`, `Data` FROM `_Streams` " + + "WHERE `Name` = '{0}'", mediaCab)) + { + streamView.Execute(); + Record streamRec = streamView.Fetch(); + if(streamRec == null) + { + this.LogMessage("Stream not found: {0}", mediaCab); + throw new InstallerException("Stream not found: " + mediaCab); + } + using(streamRec) + { + this.LogMessage("extract cab {0}", mediaCab); + Directory.CreateDirectory(this.TempDirectory); + cabFile = Path.Combine(this.TempDirectory, + Path.GetFileNameWithoutExtension(mediaCab) + ".cab"); + streamRec.GetStream("Data", cabFile); + } + } + } + else + { + cabFile = Path.Combine(this.SourceDirectory, mediaCab); + } + + this.cabName = mediaCab; + this.cabMsg = "extract {0}\\{1} {2}"; + new CabInfo(cabFile).UnpackFileSet(compressedFileMap.SourcePaths, this.WorkingDirectory, + this.CabinetProgress); + ClearReadOnlyAttribute(this.WorkingDirectory, compressedFileMap.Values); + } + foreach(InstallPath fileInstallPath in uncompressedFileMap.Values) + { + string sourcePath = Path.Combine(this.SourceDirectory, fileInstallPath.SourcePath); + string extractPath = Path.Combine(this.WorkingDirectory, fileInstallPath.SourcePath); + if(Path.GetFullPath(sourcePath).ToLowerInvariant() != + Path.GetFullPath(extractPath).ToLowerInvariant()) + { + if(!File.Exists(sourcePath)) + { + this.LogMessage("Error: Uncompressed file not found: {0}", sourcePath); + throw new FileNotFoundException("Uncompressed file not found.", sourcePath); + } + else + { + this.LogMessage("copy {0} {1}", sourcePath, extractPath); + Directory.CreateDirectory(Path.GetDirectoryName(extractPath)); + File.Copy(sourcePath, extractPath, true); + } + } + else + { + if(!File.Exists(extractPath)) + { + this.LogMessage("Error: Uncompressed file not found: {0}", extractPath); + throw new FileNotFoundException("Uncompressed file not found.", extractPath); + } + } + } + } + + private void CabinetProgress(object sender, ArchiveProgressEventArgs e) + { + switch(e.ProgressType) + { + case ArchiveProgressType.StartFile: + { + string filePath = e.CurrentFileName; + if(this.filePathMap != null) + { + InstallPath fileInstallPath = this.Files[e.CurrentFileName]; + if(fileInstallPath != null) + { + filePath = fileInstallPath.SourcePath; + } + } + this.LogMessage(this.cabMsg, this.cabName, e.CurrentFileName, + Path.Combine(this.WorkingDirectory, filePath)); + } + break; + } + } + + /// + /// Updates the install package with new files from the . The + /// files must be in the relative directory matching their . + /// This method re-compresses and packages the files if necessary, and also updates the + /// following data: File.FileSize, File.Version, File.Language, MsiFileHash.HashPart* + /// + /// + /// The cabinet compression level used during re-cabbing can be configured with the + /// property. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void UpdateFiles() + { + this.UpdateFiles(null); + } + /// + /// Updates the install package with new files from the . The + /// files must be in the relative directory matching their . + /// This method re-compresses and packages the files if necessary, and also updates the + /// following data: File.FileSize, File.Version, File.Language, MsiFileHash.HashPart?. + /// + /// List of file key strings to update + /// + /// This method does not change the media structure of the package, so it may require extracting + /// and re-compressing a large cabinet just to update one file. + ///

The cabinet compression level used during re-cabbing can be configured with the + /// property.

+ ///
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void UpdateFiles(ICollection fileKeys) + { + this.ProcessFilesByMediaDisk(fileKeys, + new ProcessFilesOnOneMediaDiskHandler(this.UpdateFilesOnOneMediaDisk)); + } + + private void UpdateFilesOnOneMediaDisk(string mediaCab, + InstallPathMap compressedFileMap, InstallPathMap uncompressedFileMap) + { + if(compressedFileMap.Count > 0) + { + string cabFile = null; + bool cabFileIsTemp = false; + if(mediaCab.StartsWith("#", StringComparison.Ordinal)) + { + cabFileIsTemp = true; + mediaCab = mediaCab.Substring(1); + + using(View streamView = this.OpenView("SELECT `Name`, `Data` FROM `_Streams` " + + "WHERE `Name` = '{0}'", mediaCab)) + { + streamView.Execute(); + Record streamRec = streamView.Fetch(); + if(streamRec == null) + { + this.LogMessage("Stream not found: {0}", mediaCab); + throw new InstallerException("Stream not found: " + mediaCab); + } + using(streamRec) + { + this.LogMessage("extract cab {0}", mediaCab); + Directory.CreateDirectory(this.TempDirectory); + cabFile = Path.Combine(this.TempDirectory, + Path.GetFileNameWithoutExtension(mediaCab) + ".cab"); + streamRec.GetStream("Data", cabFile); + } + } + } + else + { + cabFile = Path.Combine(this.SourceDirectory, mediaCab); + } + + CabInfo cab = new CabInfo(cabFile); + ArrayList fileKeyList = new ArrayList(); + foreach (CabFileInfo fileInCab in cab.GetFiles()) + { + string fileKey = fileInCab.Name; + if(this.Files[fileKey] != null) + { + fileKeyList.Add(fileKey); + } + } + string[] fileKeys = (string[]) fileKeyList.ToArray(typeof(string)); + + Directory.CreateDirectory(this.TempDirectory); + + ArrayList remainingFileKeys = new ArrayList(fileKeys); + foreach(string fileKey in fileKeys) + { + InstallPath fileInstallPath = compressedFileMap[fileKey]; + if(fileInstallPath != null) + { + UpdateFileStats(fileKey, fileInstallPath); + + string filePath = Path.Combine(this.WorkingDirectory, fileInstallPath.SourcePath); + this.LogMessage("copy {0} {1}", filePath, fileKey); + File.Copy(filePath, Path.Combine(this.TempDirectory, fileKey), true); + remainingFileKeys.Remove(fileKey); + } + } + + if(remainingFileKeys.Count > 0) + { + this.cabName = mediaCab; + this.cabMsg = "extract {0}\\{1}"; + string[] remainingFileKeysArray = (string[]) remainingFileKeys.ToArray(typeof(string)); + cab.UnpackFiles(remainingFileKeysArray, this.TempDirectory, remainingFileKeysArray, + this.CabinetProgress); + } + + ClearReadOnlyAttribute(this.TempDirectory, fileKeys); + + if(!cabFileIsTemp) + { + cab = new CabInfo(Path.Combine(this.WorkingDirectory, mediaCab)); + } + this.cabName = mediaCab; + this.cabMsg = "compress {0}\\{1}"; + cab.PackFiles(this.TempDirectory, fileKeys, fileKeys, + this.CompressionLevel, this.CabinetProgress); + + if(cabFileIsTemp) + { + Record streamRec = new Record(1); + streamRec.SetStream(1, cabFile); + this.Execute(String.Format(CultureInfo.InvariantCulture, + "UPDATE `_Streams` SET `Data` = ? WHERE `Name` = '{0}'", mediaCab), + streamRec); + } + } + + foreach (KeyValuePair entry in uncompressedFileMap) + { + UpdateFileStats((string) entry.Key, (InstallPath) entry.Value); + } + } + + private void UpdateFileStats(string fileKey, InstallPath fileInstallPath) + { + string filePath = Path.Combine(this.WorkingDirectory, fileInstallPath.SourcePath); + if(!File.Exists(filePath)) + { + this.LogMessage("Updated source file not found: {0}", filePath); + throw new FileNotFoundException("Updated source file not found: " + filePath); + } + + this.LogMessage("updatestats {0}", fileKey); + + string version = Installer.GetFileVersion(filePath); + string language = Installer.GetFileLanguage(filePath); + long size = new FileInfo(filePath).Length; + + this.Execute("UPDATE `File` SET `Version` = '{0}', `Language` = '{1}', " + + "`FileSize` = {2} WHERE `File` = '{3}'", version, language, size, fileKey); + + if ((version == null || version.Length == 0) && this.Tables.Contains("MsiFileHash")) + { + int[] hash = new int[4]; + Installer.GetFileHash(filePath, hash); + this.Execute("DELETE FROM `MsiFileHash` WHERE `File_` = '{0}'", fileKey); + this.Execute("INSERT INTO `MsiFileHash` (`File_`, `Options`, `HashPart1`, `HashPart2`, " + + "`HashPart3`, `HashPart4`) VALUES ('" + fileKey + "', 0, {0}, {1}, {2}, {3})", + hash[0], hash[1], hash[2], hash[3]); + } + } + + /// + /// Consolidates a package by combining and re-compressing all files into a single + /// internal or external cabinet. + /// + /// + /// If an installation package was built from many merge modules, this + /// method can somewhat decrease package size, complexity, and installation time. + ///

This method will also convert a package with all or mostly uncompressed + /// files into a package where all files are compressed.

+ ///

If the package contains any not-yet-applied binary file patches (for + /// example, a package generated by a call to ) then + /// this method will apply the patches before compressing the updated files.

+ ///

This method edits the database summary information and the File, Media + /// and Patch tables as necessary to maintain a valid installation package.

+ ///

The cabinet compression level used during re-cabbing can be configured with the + /// property.

+ ///
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void Consolidate(string mediaCabinet) + { + this.LogMessage("Consolidating package"); + + Directory.CreateDirectory(this.TempDirectory); + + this.LogMessage("Extracting/preparing files"); + this.ProcessFilesByMediaDisk(null, + new ProcessFilesOnOneMediaDiskHandler(this.PrepareOneMediaDiskForConsolidation)); + + this.LogMessage("Applying any file patches"); + ApplyFilePatchesForConsolidation(); + + this.LogMessage("Clearing PatchPackage, Patch, MsiPatchHeaders tables"); + if (this.Tables.Contains("PatchPackage")) + { + this.Execute("DELETE FROM `PatchPackage` WHERE `PatchId` <> ''"); + } + if (this.Tables.Contains("Patch")) + { + this.Execute("DELETE FROM `Patch` WHERE `File_` <> ''"); + } + if (this.Tables.Contains("MsiPatchHeaders")) + { + this.Execute("DELETE FROM `MsiPatchHeaders` WHERE `StreamRef` <> ''"); + } + + this.LogMessage("Resequencing files"); + ArrayList files = new ArrayList(); + using(View fileView = this.OpenView("SELECT `File`, `Attributes`, `Sequence` " + + "FROM `File` ORDER BY `Sequence`")) + { + fileView.Execute(); + + foreach (Record fileRec in fileView) using(fileRec) + { + files.Add(fileRec[1]); + int fileAttributes = fileRec.GetInteger(2); + fileAttributes &= ~(int) (FileAttributes.Compressed + | FileAttributes.NonCompressed | FileAttributes.PatchAdded); + fileRec[2] = fileAttributes; + fileRec[3] = files.Count; + fileView.Update(fileRec); + } + } + + if (mediaCabinet== null) { + throw new ArgumentNullException("mediaCabinet"); + } + + bool internalCab = false; + if(mediaCabinet.StartsWith("#", StringComparison.Ordinal)) + { + internalCab = true; + mediaCabinet = mediaCabinet.Substring(1); + } + + this.LogMessage("Cabbing files"); + string[] fileKeys = (string[]) files.ToArray(typeof(string)); + string cabPath = Path.Combine(internalCab ? this.TempDirectory + : this.WorkingDirectory, mediaCabinet); + this.cabName = mediaCabinet; + this.cabMsg = "compress {0}\\{1}"; + new CabInfo(cabPath).PackFiles(this.TempDirectory, fileKeys, + fileKeys, this.CompressionLevel, this.CabinetProgress); + + this.DeleteEmbeddedCabs(); + + if(internalCab) + { + this.LogMessage("Inserting cab stream into MSI"); + Record cabRec = new Record(1); + cabRec.SetStream(1, cabPath); + this.Execute("INSERT INTO `_Streams` (`Name`, `Data`) VALUES ('" + mediaCabinet + "', ?)", cabRec); + } + + this.LogMessage("Inserting cab media record into MSI"); + this.Execute("DELETE FROM `Media` WHERE `DiskId` <> 0"); + this.Execute("INSERT INTO `Media` (`DiskId`, `LastSequence`, `Cabinet`) " + + "VALUES (1, " + files.Count + ", '" + (internalCab ? "#" : "") + mediaCabinet + "')"); + + + this.LogMessage("Setting compressed flag on package summary info"); + this.SummaryInfo.WordCount = this.SummaryInfo.WordCount | 2; + this.SummaryInfo.Persist(); + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + private void DeleteEmbeddedCabs() + { + using (View view = this.OpenView("SELECT `Cabinet` FROM `Media` WHERE `Cabinet` <> ''")) + { + view.Execute(); + + foreach (Record rec in view) using(rec) + { + string cab = rec.GetString(1); + if(cab.StartsWith("#", StringComparison.Ordinal)) + { + cab = cab.Substring(1); + this.LogMessage("Deleting embedded cab stream: {0}", cab); + this.Execute("DELETE FROM `_Streams` WHERE `Name` = '{0}'", cab); + } + } + } + } + + private void PrepareOneMediaDiskForConsolidation(string mediaCab, + InstallPathMap compressedFileMap, InstallPathMap uncompressedFileMap) + { + if(compressedFileMap.Count > 0) + { + string cabFile = null; + if(mediaCab.StartsWith("#", StringComparison.Ordinal)) + { + mediaCab = mediaCab.Substring(1); + + using (View streamView = this.OpenView("SELECT `Name`, `Data` FROM `_Streams` " + + "WHERE `Name` = '{0}'", mediaCab)) + { + streamView.Execute(); + Record streamRec = streamView.Fetch(); + if(streamRec == null) + { + this.LogMessage("Stream not found: {0}", mediaCab); + throw new InstallerException("Stream not found: " + mediaCab); + } + using(streamRec) + { + this.LogMessage("extract cab {0}", mediaCab); + cabFile = Path.Combine(this.TempDirectory, + Path.GetFileNameWithoutExtension(mediaCab) + ".cab"); + streamRec.GetStream("Data", cabFile); + } + } + } + else + { + cabFile = Path.Combine(this.SourceDirectory, mediaCab); + } + string[] fileKeys = new string[compressedFileMap.Keys.Count]; + compressedFileMap.Keys.CopyTo(fileKeys, 0); + this.cabName = mediaCab; + this.cabMsg = "extract {0}\\{1}"; + new CabInfo(cabFile).UnpackFiles(fileKeys, this.TempDirectory, fileKeys, + this.CabinetProgress); + ClearReadOnlyAttribute(this.TempDirectory, fileKeys); + } + foreach (KeyValuePair entry in uncompressedFileMap) + { + string fileKey = (string) entry.Key; + InstallPath fileInstallPath = (InstallPath) entry.Value; + + string filePath = Path.Combine(this.SourceDirectory, fileInstallPath.SourcePath); + this.LogMessage("copy {0} {1}", filePath, fileKey); + File.Copy(filePath, Path.Combine(this.TempDirectory, fileKey)); + } + } + + private void ClearReadOnlyAttribute(string baseDirectory, IEnumerable filePaths) + { + foreach(object filePath in filePaths) + { + string fullFilePath = Path.Combine(baseDirectory, filePath.ToString()); + if (File.Exists(fullFilePath)) + { + System.IO.FileAttributes fileAttributes = File.GetAttributes(fullFilePath); + if ((fileAttributes & System.IO.FileAttributes.ReadOnly) != 0) + { + fileAttributes &= ~System.IO.FileAttributes.ReadOnly; + File.SetAttributes(fullFilePath, fileAttributes); + } + } + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + private void ApplyFilePatchesForConsolidation() + { + if(this.Tables.Contains("Patch")) + { + using(View patchView = this.OpenView("SELECT `File_`, `Sequence` " + + "FROM `Patch` ORDER BY `Sequence`")) + { + patchView.Execute(); + Hashtable extractedPatchCabs = new Hashtable(); + + foreach (Record patchRec in patchView) using(patchRec) + { + string fileKey = (string) patchRec[1]; + int sequence = patchRec.GetInteger(2); + this.LogMessage("patch {0}", fileKey); + + string tempPatchFile = Path.Combine(this.TempDirectory, fileKey + ".pat"); + ExtractFilePatch(fileKey, sequence, tempPatchFile, extractedPatchCabs); + string filePath = Path.Combine(this.TempDirectory, fileKey); + string oldFilePath = filePath + ".old"; + if(File.Exists(oldFilePath)) File.Delete(oldFilePath); + File.Move(filePath, oldFilePath); + Type.GetType("Microsoft.Deployment.WindowsInstaller.FilePatch") + .GetMethod("ApplyPatchToFile", + new Type[] { typeof(string), typeof(string), typeof(string) }) + .Invoke(null, new object[] { tempPatchFile, oldFilePath, filePath }); + } + } + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + private void ExtractFilePatch(string fileKey, int sequence, string extractPath, + IDictionary extractedCabs) + { + string mediaCab = null; + using(View mediaView = this.OpenView("SELECT `DiskId`, `LastSequence`, `Cabinet` " + + "FROM `Media` ORDER BY `DiskId`")) + { + mediaView.Execute(); + + foreach (Record mediaRec in mediaView) using(mediaRec) + { + int mediaMaxSequence = mediaRec.GetInteger(2); + if(mediaMaxSequence >= sequence) + { + mediaCab = mediaRec.GetString(3); + break; + } + } + } + + if(mediaCab == null || mediaCab.Length == 0) + { + this.LogMessage("Could not find cabinet for file patch: {0}", fileKey); + throw new InstallerException("Could not find cabinet for file patch: " + fileKey); + } + + if(!mediaCab.StartsWith("#", StringComparison.Ordinal)) + { + this.LogMessage("Error: Patch cabinet {0} must be embedded", mediaCab); + throw new InstallerException("Patch cabinet " + mediaCab + " must be embedded."); + } + mediaCab = mediaCab.Substring(1); + + string cabFile = (string) extractedCabs[mediaCab]; + if(cabFile == null) + { + using(View streamView = this.OpenView("SELECT `Name`, `Data` FROM `_Streams` " + + "WHERE `Name` = '{0}'", mediaCab)) + { + streamView.Execute(); + Record streamRec = streamView.Fetch(); + if(streamRec == null) + { + this.LogMessage("Stream not found: {0}", mediaCab); + throw new InstallerException("Stream not found: " + mediaCab); + } + using(streamRec) + { + this.LogMessage("extract cab {0}", mediaCab); + Directory.CreateDirectory(this.TempDirectory); + cabFile = Path.Combine(this.TempDirectory, + Path.GetFileNameWithoutExtension(mediaCab) + ".cab"); + streamRec.GetStream("Data", cabFile); + } + } + extractedCabs[mediaCab] = cabFile; + } + + this.LogMessage("extract patch {0}\\{1}", mediaCab, fileKey); + new CabInfo(cabFile).UnpackFile(fileKey, extractPath); + } + + /// + /// Rebuilds the cached directory structure information accessed by the + /// and properties. This + /// should be done after modifying the File, Component, or Directory + /// tables, or else the cached information may no longer be accurate. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void UpdateDirectories() + { + this.dirPathMap = null; + this.filePathMap = InstallPathMap.BuildFilePathMap(this, + InstallPathMap.BuildComponentPathMap(this, this.Directories), false); + } + + /// + /// Gets a mapping from Directory keys to source/target paths. + /// + /// + /// If the Directory table is modified, this mapping + /// will be outdated until you call . + /// + public InstallPathMap Directories + { + get + { + if(this.dirPathMap == null) + { + this.dirPathMap = InstallPathMap.BuildDirectoryPathMap(this, false); + } + return this.dirPathMap; + } + } + private InstallPathMap dirPathMap; + + /// + /// Gets a mapping from File keys to source/target paths. + /// + /// + /// If the File, Component, or Directory tables are modified, this mapping + /// may be outdated until you call . + /// + public InstallPathMap Files + { + get + { + if(this.filePathMap == null) + { + this.filePathMap = InstallPathMap.BuildFilePathMap(this, + InstallPathMap.BuildComponentPathMap(this, this.Directories), false); + } + return this.filePathMap; + } + } + private InstallPathMap filePathMap; + + /// + /// Gets or sets the compression level used by + /// and . + /// + /// + /// If the Directory table is modified, this mapping will be outdated + /// until you close and reopen the install package. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public CompressionLevel CompressionLevel + { + get { return this.compressionLevel; } + set { this.compressionLevel = value; } + } + private CompressionLevel compressionLevel; + + /// + /// Applies a patch package to the database, resulting in an installation package that + /// has the patch built-in. + /// + /// The patch package to be applied + /// Optional name of the specific transform to apply. + /// This parameter is usually left null, which causes the patch to be searched for + /// a transform that is valid to apply to this database. + /// + /// If the patch contains any binary file patches, they will not immediately be applied + /// to the target files, though they will at installation time. + ///

After calling this method you can use to apply + /// the file patches immediately and also discard any outdated files from the package.

+ ///
+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void ApplyPatch(PatchPackage patchPackage, string transform) + { + if(patchPackage == null) throw new ArgumentNullException("patchPackage"); + + this.LogMessage("Applying patch file {0} to database {1}", + patchPackage.FilePath, this.FilePath); + + if(transform == null) + { + this.LogMessage("No transform specified; searching for valid patch transform"); + string[] validTransforms = patchPackage.GetValidTransforms(this); + if(validTransforms.Length == 0) + { + this.LogMessage("No valid patch transform was found"); + throw new InvalidOperationException("No valid patch transform was found."); + } + transform = validTransforms[0]; + } + this.LogMessage("Patch transform = {0}", transform); + + string patchPrefix = Path.GetFileNameWithoutExtension(patchPackage.FilePath) + "_"; + + string specialTransform = "#" + transform; + Directory.CreateDirectory(this.TempDirectory); + this.LogMessage("Extracting substorage {0}", transform); + string transformFile = Path.Combine(this.TempDirectory, + patchPrefix + Path.GetFileNameWithoutExtension(transform) + ".mst"); + patchPackage.ExtractTransform(transform, transformFile); + this.LogMessage("Extracting substorage {0}", specialTransform); + string specialTransformFile = Path.Combine(this.TempDirectory, + patchPrefix + Path.GetFileNameWithoutExtension(specialTransform) + ".mst"); + patchPackage.ExtractTransform(specialTransform, specialTransformFile); + + if (this.Tables.Contains("Patch") && !this.Tables["Patch"].Columns.Contains("_StreamRef")) + { + if(this.CountRows("Patch") > 0) + { + this.LogMessage("Warning: non-empty Patch table exists without StreamRef_ column; " + + "patch transform may fail"); + } + else + { + this.Execute("DROP TABLE `Patch`"); + this.Execute("CREATE TABLE `Patch` (`File_` CHAR(72) NOT NULL, " + + "`Sequence` INTEGER NOT NULL, `PatchSize` LONG NOT NULL, " + + "`Attributes` INTEGER NOT NULL, `Header` OBJECT, `StreamRef_` CHAR(72) " + + "PRIMARY KEY `File_`, `Sequence`)"); + } + } + + this.LogMessage("Applying transform {0} to database", transform); + this.ApplyTransform(transformFile); + this.LogMessage("Applying transform {0} to database", specialTransform); + this.ApplyTransform(specialTransformFile); + + if (this.Tables.Contains("MsiPatchHeaders") && this.CountRows("MsiPatchHeaders") > 0 && + (!this.Tables.Contains("Patch") || this.CountRows("Patch", "`StreamRef_` <> ''") == 0)) + { + this.LogMessage("Error: patch transform failed because of missing Patch.StreamRef_ column"); + throw new InstallerException("Patch transform failed because of missing Patch.StreamRef_ column"); + } + + IList mediaIds = this.ExecuteIntegerQuery("SELECT `Media_` FROM `PatchPackage` " + + "WHERE `PatchId` = '{0}'", patchPackage.PatchCode); + if (mediaIds.Count == 0) + { + this.LogMessage("Warning: PatchPackage Media record not found -- " + + "skipping inclusion of patch cabinet"); + } + else + { + int patchMediaDiskId = mediaIds[0]; + IList patchCabinets = this.ExecuteStringQuery("SELECT `Cabinet` FROM `Media` " + + "WHERE `DiskId` = {0}", patchMediaDiskId); + if(patchCabinets.Count == 0) + { + this.LogMessage("Patch cabinet record not found"); + throw new InstallerException("Patch cabinet record not found."); + } + string patchCabinet = patchCabinets[0]; + this.LogMessage("Patch cabinet = {0}", patchCabinet); + if(!patchCabinet.StartsWith("#", StringComparison.Ordinal)) + { + this.LogMessage("Error: Patch cabinet must be embedded"); + throw new InstallerException("Patch cabinet must be embedded."); + } + patchCabinet = patchCabinet.Substring(1); + + string renamePatchCabinet = patchPrefix + patchCabinet; + + const int HIGH_DISKID = 30000; // Must not collide with other patch media DiskIDs + int renamePatchMediaDiskId = HIGH_DISKID; + while (this.CountRows("Media", "`DiskId` = " + renamePatchMediaDiskId) > 0) renamePatchMediaDiskId++; + + // Since the patch cab is now embedded in the MSI, it shouldn't have a separate disk prompt/source + this.LogMessage("Renaming the patch media record"); + int lastSeq = Convert.ToInt32(this.ExecuteScalar("SELECT `LastSequence` FROM `Media` WHERE `DiskId` = {0}", patchMediaDiskId),CultureInfo.InvariantCulture); + this.Execute("DELETE FROM `Media` WHERE `DiskId` = {0}", patchMediaDiskId); + this.Execute("INSERT INTO `Media` (`DiskId`, `LastSequence`, `Cabinet`) VALUES ({0}, '{1}', '#{2}')", + renamePatchMediaDiskId, lastSeq, renamePatchCabinet); + this.Execute("UPDATE `PatchPackage` SET `Media_` = {0} WHERE `PatchId` = '{1}'", renamePatchMediaDiskId, patchPackage.PatchCode); + + this.LogMessage("Copying patch cabinet: {0}", patchCabinet); + string patchCabFile = Path.Combine(this.TempDirectory, + Path.GetFileNameWithoutExtension(patchCabinet) + ".cab"); + using(View streamView = patchPackage.OpenView("SELECT `Name`, `Data` FROM `_Streams` " + + "WHERE `Name` = '{0}'", patchCabinet)) + { + streamView.Execute(); + Record streamRec = streamView.Fetch(); + if(streamRec == null) + { + this.LogMessage("Error: Patch cabinet not found"); + throw new InstallerException("Patch cabinet not found."); + } + using(streamRec) + { + streamRec.GetStream(2, patchCabFile); + } + } + using(Record patchCabRec = new Record(2)) + { + patchCabRec[1] = patchCabinet; + patchCabRec.SetStream(2, patchCabFile); + this.Execute("INSERT INTO `_Streams` (`Name`, `Data`) VALUES (?, ?)", patchCabRec); + } + + this.LogMessage("Ensuring PatchFiles action exists in InstallExecuteSequence table"); + if (this.Tables.Contains("InstallExecuteSequence")) + { + if(this.CountRows("InstallExecuteSequence", "`Action` = 'PatchFiles'") == 0) + { + IList installFilesSeqList = this.ExecuteIntegerQuery("SELECT `Sequence` " + + "FROM `InstallExecuteSequence` WHERE `Action` = 'InstallFiles'"); + short installFilesSeq = (short) (installFilesSeqList.Count != 0 ? + installFilesSeqList[0] : 0); + this.Execute("INSERT INTO `InstallExecuteSequence` (`Action`, `Sequence`) " + + "VALUES ('PatchFiles', {0})", installFilesSeq + 1); + } + } + + // Patch-added files need to be marked always-compressed + this.LogMessage("Adjusting attributes of patch-added files"); + using(View fileView = this.OpenView("SELECT `File`, `Attributes`, `Sequence` " + + "FROM `File` ORDER BY `Sequence`")) + { + fileView.Execute(); + + foreach (Record fileRec in fileView) using(fileRec) + { + int fileAttributes = fileRec.GetInteger(2); + if ((fileAttributes & (int) FileAttributes.PatchAdded) != 0) + { + fileAttributes = (fileAttributes | (int) FileAttributes.Compressed) + & ~(int) FileAttributes.NonCompressed + & ~(int) FileAttributes.PatchAdded; + fileRec[2] = fileAttributes; + fileView.Update(fileRec); + } + } + } + } + + this.LogMessage("Applying new summary info from patch package"); + this.SummaryInfo.RevisionNumber = this.Property["PATCHNEWPACKAGECODE"]; + this.SummaryInfo.Subject = this.Property["PATCHNEWSUMMARYSUBJECT"]; + this.SummaryInfo.Comments = this.Property["PATCHNEWSUMMARYCOMMENTS"]; + this.SummaryInfo.Persist(); + this.Property["PATCHNEWPACKAGECODE" ] = null; + this.Property["PATCHNEWSUMMARYSUBJECT" ] = null; + this.Property["PATCHNEWSUMMARYCOMMENTS"] = null; + + this.LogMessage("Patch application finished"); + } + + /// + /// Accessor for getting and setting properties of the InstallPackage database. + /// + public InstallPackageProperties Property + { + get + { + if(this.properties == null) + { + this.properties = new InstallPackageProperties(this); + } + return this.properties; + } + } + private InstallPackageProperties properties = null; +} + +/// +/// Accessor for getting and setting properties of the database. +/// +internal class InstallPackageProperties +{ + internal InstallPackageProperties(InstallPackage installPackage) + { + this.installPackage = installPackage; + } + private InstallPackage installPackage; + + /// + /// Gets or sets a property in the database. When getting a property + /// that does not exist in the database, an empty string is returned. + /// To remove a property from the database, set it to an empty string. + /// + /// + /// This has the same results as direct SQL queries on the Property table; it's only + /// meant to be a more convenient way of access. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string this[string name] + { + get + { + IList values = installPackage.ExecuteStringQuery( + "SELECT `Value` FROM `Property` WHERE `Property` = '{0}'", name); + return (values.Count != 0 ? values[0] : ""); + } + set + { + Record propRec = new Record(name, (value != null ? value : "")); + installPackage.Execute("DELETE FROM `Property` WHERE `Property` = ?", propRec); + if(value != null && value.Length != 0) + { + installPackage.Execute("INSERT INTO `Property` (`Property`, `Value`) VALUES (?, ?)", + propRec); + } + } + } +} + +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Package/InstallPath.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Package/InstallPath.cs new file mode 100644 index 000000000..056db9ee2 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Package/InstallPath.cs @@ -0,0 +1,1084 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller.Package +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.IO; + + /// + /// Represents the installation path of a file or directory from an installer product database. + /// + internal class InstallPath + { + /// + /// Creates a new InstallPath, specifying a filename. + /// + /// The name of the file or directory. Not a full path. + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public InstallPath(string name) : this(name, false) { } + + /// + /// Creates a new InstallPath, parsing out either the short or long filename. + /// + /// The name of the file or directory, in short|long syntax for a filename + /// or targetshort|targetlong:sourceshort|sourcelong syntax for a directory. + /// true to parse the short part of the combined filename; false to parse the long part + public InstallPath(string name, bool useShortNames) + { + if(name == null) + { + throw new ArgumentNullException("name"); + } + this.parentPath = null; + ParseName(name, useShortNames); + } + + private void ParseName(string name, bool useShortNames) + { + string[] parse = name.Split(new char[] { ':' }, 3); + if(parse.Length == 3) + { + // Syntax was targetshort:sourceshort|targetlong:sourcelong. + // Chnage it to targetshort|targetlong:sourceshort|sourcelong. + parse = name.Split(new char[] { ':', '|' }, 4); + if(parse.Length == 4) + parse = new string[] { parse[0] + '|' + parse[2], parse[1] + '|' + parse[3] }; + else + parse = new string[] { parse[0] + '|' + parse[1], parse[1] + '|' + parse[2] }; + } + string targetName = parse[0]; + string sourceName = (parse.Length == 2 ? parse[1] : parse[0]); + parse = targetName.Split(new char[] { '|' }, 2); + if(parse.Length == 2) targetName = (useShortNames ? parse[0] : parse[1]); + parse = sourceName.Split(new char[] { '|' }, 2); + if(parse.Length == 2) sourceName = (useShortNames ? parse[0] : parse[1]); + + this.SourceName = sourceName; + this.TargetName = targetName; + } + + /// + /// Gets the path of the parent directory. + /// + public InstallPath ParentPath + { + get + { + return parentPath; + } + } + internal void SetParentPath(InstallPath value) + { + parentPath = value; + ResetSourcePath(); + ResetTargetPath(); + } + private InstallPath parentPath; + + /// + /// Gets the set of child paths if this InstallPath object represents a a directory. + /// + public InstallPathCollection ChildPaths + { + get + { + if(childPaths == null) + { + childPaths = new InstallPathCollection(this); + } + return childPaths; + } + } + private InstallPathCollection childPaths; + + /// + /// Gets or sets the source name of the InstallPath. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string SourceName + { + get + { + return sourceName; + } + set + { + if(value == null) + { + throw new ArgumentNullException("value"); + } + sourceName = value; + ResetSourcePath(); + } + } + private string sourceName; + + /// + /// Gets or sets the target name of the install path. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string TargetName + { + get + { + return targetName; + } + set + { + if(value == null) + { + throw new ArgumentNullException("value"); + } + targetName = value; + ResetTargetPath(); + } + } + private string targetName; + + /// + /// Gets the full source path. + /// + public string SourcePath + { + get + { + if(sourcePath == null) + { + if(parentPath != null) + { + sourcePath = (sourceName.Equals(".") ? parentPath.SourcePath + : Path.Combine(parentPath.SourcePath, sourceName)); + } + else + { + sourcePath = sourceName; + } + } + return sourcePath; + } + set + { + ResetSourcePath(); + sourcePath = value; + } + } + private string sourcePath; + + /// + /// Gets the full target path. + /// + public string TargetPath + { + get + { + if(targetPath == null) + { + if(parentPath != null) + { + targetPath = (targetName.Equals(".") ? parentPath.TargetPath + : Path.Combine(parentPath.TargetPath, targetName)); + } + else + { + targetPath = targetName; + } + } + return targetPath; + } + set + { + ResetTargetPath(); + targetPath = value; + } + } + private string targetPath; + + private void ResetSourcePath() + { + if(sourcePath != null) + { + sourcePath = null; + if(childPaths != null) + { + foreach(InstallPath ip in childPaths) + { + ip.ResetSourcePath(); + } + } + } + } + + private void ResetTargetPath() + { + if(targetPath != null) + { + targetPath = null; + if(childPaths != null) + { + foreach(InstallPath ip in childPaths) + { + ip.ResetTargetPath(); + } + } + } + } + + /// + /// Gets the full source path. + /// + /// + public override String ToString() + { + return SourcePath; + } + } + + /// + /// Represents a collection of InstallPaths that are the child paths of the same parent directory. + /// + internal class InstallPathCollection : IList + { + private InstallPath parentPath; + private List items; + + internal InstallPathCollection(InstallPath parentPath) + { + this.parentPath = parentPath; + this.items = new List(); + } + + /// + /// Gets or sets the element at the specified index. + /// + public InstallPath this[int index] + { + get + { + return this.items[index]; + } + set + { + this.OnSet(this.items[index], value); + this.items[index] = value; + } + } + + /// + /// Adds a new child path to the collection. + /// + /// The InstallPath to add. + public void Add(InstallPath item) + { + this.OnInsert(item); + this.items.Add(item); + } + + /// + /// Removes a child path to the collection. + /// + /// The InstallPath to remove. + public bool Remove(InstallPath item) + { + int index = this.items.IndexOf(item); + if (index >= 0) + { + this.OnRemove(item); + this.items.RemoveAt(index); + return true; + } + else + { + return false; + } + } + + /// + /// Gets the index of a child path in the collection. + /// + /// The InstallPath to search for. + /// The index of the item, or -1 if not found. + public int IndexOf(InstallPath item) + { + return this.items.IndexOf(item); + } + + /// + /// Inserts a child path into the collection. + /// + /// The insertion index. + /// The InstallPath to insert. + public void Insert(int index, InstallPath item) + { + this.OnInsert(item); + this.items.Insert(index, item); + } + + /// + /// Tests if the collection contains a child path. + /// + /// The InstallPath to search for. + /// true if the item is found; false otherwise + public bool Contains(InstallPath item) + { + return this.items.Contains(item); + } + + /// + /// Copies the collection into an array. + /// + /// The array to copy into. + /// The starting index in the destination array. + public void CopyTo(InstallPath[] array, int index) + { + this.items.CopyTo(array, index); + } + + private void OnInsert(InstallPath item) + { + if (item.ParentPath != null) + { + item.ParentPath.ChildPaths.Remove(item); + } + + item.SetParentPath(this.parentPath); + } + + private void OnRemove(InstallPath item) + { + item.SetParentPath(null); + } + + private void OnSet(InstallPath oldItem, InstallPath newItem) + { + this.OnRemove(oldItem); + this.OnInsert(newItem); + } + + /// + /// Removes an item from the collection. + /// + /// The index of the item to remove. + public void RemoveAt(int index) + { + this.OnRemove(this[index]); + this.items.RemoveAt(index); + } + + /// + /// Removes all items from the collection. + /// + public void Clear() + { + foreach (InstallPath item in this) + { + this.OnRemove(item); + } + + this.items.Clear(); + } + + /// + /// Gets the number of items in the collection. + /// + public int Count + { + get + { + return this.items.Count; + } + } + + bool ICollection.IsReadOnly + { + get + { + return false; + } + } + + /// + /// Gets an enumerator over all items in the collection. + /// + /// An enumerator for the collection. + public IEnumerator GetEnumerator() + { + return this.items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable) this).GetEnumerator(); + } + } + + /// + /// Represents a mapping of install paths for all directories, components, or files in + /// an installation database. + /// + internal class InstallPathMap : IDictionary + { + /// + /// Builds a mapping from File keys to installation paths. + /// + /// Installation database. + /// Component mapping returned by . + /// true to use short file names; false to use long names + /// An InstallPathMap with the described mapping. + public static InstallPathMap BuildFilePathMap(Database db, InstallPathMap componentPathMap, + bool useShortNames) + { + if(db == null) + { + throw new ArgumentNullException("db"); + } + + if(componentPathMap == null) + { + componentPathMap = BuildComponentPathMap(db, BuildDirectoryPathMap(db, useShortNames)); + } + + InstallPathMap filePathMap = new InstallPathMap(); + + using (View fileView = db.OpenView("SELECT `File`, `Component_`, `FileName` FROM `File`")) + { + fileView.Execute(); + + foreach (Record fileRec in fileView) + { + using (fileRec) + { + string file = (string) fileRec[1]; + string comp = (string) fileRec[2]; + string fileName = (string) fileRec[3]; + + InstallPath compPath = (InstallPath) componentPathMap[comp]; + if(compPath != null) + { + InstallPath filePath = new InstallPath(fileName, useShortNames); + compPath.ChildPaths.Add(filePath); + filePathMap[file] = filePath; + } + } + } + } + + return filePathMap; + } + + /// + /// Builds a mapping from Component keys to installation paths. + /// + /// Installation database. + /// Directory mapping returned by + /// . + /// An InstallPathMap with the described mapping. + public static InstallPathMap BuildComponentPathMap(Database db, InstallPathMap directoryPathMap) + { + if(db == null) + { + throw new ArgumentNullException("db"); + } + + InstallPathMap compPathMap = new InstallPathMap(); + + using (View compView = db.OpenView("SELECT `Component`, `Directory_` FROM `Component`")) + { + compView.Execute(); + + foreach (Record compRec in compView) + { + using (compRec) + { + string comp = (string) compRec[1]; + InstallPath dirPath = (InstallPath) directoryPathMap[(string) compRec[2]]; + + if (dirPath != null) + { + compPathMap[comp] = dirPath; + } + } + } + } + + return compPathMap; + } + + /// + /// Builds a mapping from Directory keys to installation paths. + /// + /// Installation database. + /// true to use short directory names; false to use long names + /// An InstallPathMap with the described mapping. + public static InstallPathMap BuildDirectoryPathMap(Database db, bool useShortNames) + { + return BuildDirectoryPathMap(db, useShortNames, null, null); + } + + /// + /// Builds a mapping of Directory keys to directory paths, specifying root directories + /// for the source and target paths. + /// + /// Database containing the Directory table. + /// true to use short directory names; false to use long names + /// The root directory path of all source paths, or null to leave them relative. + /// The root directory path of all source paths, or null to leave them relative. + /// An InstallPathMap with the described mapping. + public static InstallPathMap BuildDirectoryPathMap(Database db, bool useShortNames, + string sourceRootDir, string targetRootDir) + { + if(db == null) + { + throw new ArgumentNullException("db"); + } + + if(sourceRootDir == null) sourceRootDir = ""; + if(targetRootDir == null) targetRootDir = ""; + + InstallPathMap dirMap = new InstallPathMap(); + IDictionary dirTreeMap = new Hashtable(); + + using (View dirView = db.OpenView("SELECT `Directory`, `Directory_Parent`, `DefaultDir` FROM `Directory`")) + { + dirView.Execute(); + + foreach (Record dirRec in dirView) using (dirRec) + { + string key = (string) dirRec[1]; + string parentKey = (string) dirRec[2]; + InstallPath dir = new InstallPath((string) dirRec[3], useShortNames); + + dirMap[key] = dir; + + InstallPathMap siblingDirs = (InstallPathMap) dirTreeMap[parentKey]; + if (siblingDirs == null) + { + siblingDirs = new InstallPathMap(); + dirTreeMap[parentKey] = siblingDirs; + } + siblingDirs.Add(key, dir); + } + } + + foreach (KeyValuePair entry in (InstallPathMap) dirTreeMap[""]) + { + string key = (string) entry.Key; + InstallPath dir = (InstallPath) entry.Value; + LinkSubdirectories(key, dir, dirTreeMap); + } + + InstallPath targetDirPath = (InstallPath) dirMap["TARGETDIR"]; + if(targetDirPath != null) + { + targetDirPath.SourcePath = sourceRootDir; + targetDirPath.TargetPath = targetRootDir; + } + + return dirMap; + } + + private static void LinkSubdirectories(string key, InstallPath dir, IDictionary dirTreeMap) + { + InstallPathMap subDirs = (InstallPathMap) dirTreeMap[key]; + if(subDirs != null) + { + foreach (KeyValuePair entry in subDirs) + { + string subKey = (string) entry.Key; + InstallPath subDir = (InstallPath) entry.Value; + dir.ChildPaths.Add(subDir); + LinkSubdirectories(subKey, subDir, dirTreeMap); + } + } + } + + private Dictionary items; + + /// + /// Creates a new empty InstallPathMap. + /// + public InstallPathMap() + { + this.items = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Gets a mapping from keys to source paths. + /// + public IDictionary SourcePaths + { + get + { + return new SourcePathMap(this); + } + } + + /// + /// Gets a mapping from keys to target paths. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public IDictionary TargetPaths + { + get + { + return new TargetPathMap(this); + } + } + + /// + /// Gets or sets an install path for a direcotry, component, or file key. + /// + /// Depending on the type of InstallPathMap, this is the primary key from the + /// either the Directory, Component, or File table. + /// + /// Changing an install path does not modify the Database used to generate this InstallPathMap. + /// + public InstallPath this[string key] + { + get + { + InstallPath value = null; + this.items.TryGetValue(key, out value); + return value; + } + set + { + this.items[key] = value; + } + } + + /// + /// Gets the collection of keys in the InstallPathMap. Depending on the type of InstallPathMap, + /// they are all directory, component, or file key strings. + /// + public ICollection Keys + { + get + { + return this.items.Keys; + } + } + + /// + /// Gets the collection of InstallPath values in the InstallPathMap. + /// + public ICollection Values + { + get + { + return this.items.Values; + } + } + + /// + /// Sets an install path for a direcotry, component, or file key. + /// + /// Depending on the type of InstallPathMap, this is the primary key from the + /// either the Directory, Component, or File table. + /// The install path of the key item. + /// + /// Changing an install path does not modify the Database used to generate this InstallPathMap. + /// + public void Add(string key, InstallPath installPath) + { + this.items.Add(key, installPath); + } + + /// + /// Removes an install path from the map. + /// + /// Depending on the type of InstallPathMap, this is the primary key from the + /// either the Directory, Component, or File table. + /// true if the item was removed, false if it did not exist + /// + /// Changing an install path does not modify the Database used to generate this InstallPathMap. + /// + public bool Remove(string key) + { + return this.items.Remove(key); + } + + /// + /// Tests whether a direcotry, component, or file key exists in the map. + /// + /// Depending on the type of InstallPathMap, this is the primary key from the + /// either the Directory, Component, or File table. + /// true if the key is found; false otherwise + public bool ContainsKey(string key) + { + return this.items.ContainsKey(key); + } + + /* + public override string ToString() + { + System.Text.StringBuilder buf = new System.Text.StringBuilder(); + foreach(KeyValuePair entry in this) + { + buf.AppendFormat("{0}={1}", entry.Key, entry.Value); + buf.Append("\n"); + } + return buf.ToString(); + } + */ + + /// + /// Attempts to get a value from the dictionary. + /// + /// The key to lookup. + /// Receives the value, or null if they key was not found. + /// True if the value was found, else false. + public bool TryGetValue(string key, out InstallPath value) + { + return this.items.TryGetValue(key, out value); + } + + void ICollection>.Add(KeyValuePair item) + { + ((ICollection>) this.items).Add(item); + } + + /// + /// Removes all entries from the dictionary. + /// + public void Clear() + { + this.items.Clear(); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return ((ICollection>) this.items).Contains(item); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((ICollection>) this.items).CopyTo(array, arrayIndex); + } + + /// + /// Gets the number of entries in the dictionary. + /// + public int Count + { + get + { + return this.items.Count; + } + } + + bool ICollection>.IsReadOnly + { + get + { + return false; + } + } + + bool ICollection>.Remove(KeyValuePair item) + { + return ((ICollection>) this.items).Remove(item); + } + + /// + /// Gets an enumerator over all entries in the dictionary. + /// + /// An enumerator for the dictionary. + public IEnumerator> GetEnumerator() + { + return this.items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.items.GetEnumerator(); + } + } + + internal class SourcePathMap : IDictionary + { + private const string RO_MSG = + "The SourcePathMap collection is read-only. " + + "Modify the InstallPathMap instead."; + + private InstallPathMap map; + + internal SourcePathMap(InstallPathMap map) + { + this.map = map; + } + + public void Add(string key, string value) + { + throw new InvalidOperationException(RO_MSG); + } + + public bool ContainsKey(string key) + { + return this.map.ContainsKey(key); + } + + public ICollection Keys + { + get + { + return this.map.Keys; + } + } + + public bool Remove(string key) + { + throw new InvalidOperationException(RO_MSG); + } + + public bool TryGetValue(string key, out string value) + { + InstallPath installPath; + if (this.map.TryGetValue(key, out installPath)) + { + value = installPath.SourcePath; + return true; + } + else + { + value = null; + return false; + } + } + + public ICollection Values + { + get + { + List values = new List(this.Count); + foreach (KeyValuePair entry in this.map) + { + values.Add(entry.Value.SourcePath); + } + return values; + } + } + + public string this[string key] + { + get + { + string value = null; + this.TryGetValue(key, out value); + return value; + } + set + { + throw new InvalidOperationException(RO_MSG); + } + } + + public void Add(KeyValuePair item) + { + throw new InvalidOperationException(RO_MSG); + } + + public void Clear() + { + throw new InvalidOperationException(RO_MSG); + } + + public bool Contains(KeyValuePair item) + { + string value = this[item.Key]; + return value == item.Value; + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + foreach (KeyValuePair entry in this) + { + array[arrayIndex] = entry; + arrayIndex++; + } + } + + public int Count + { + get + { + return this.map.Count; + } + } + + public bool IsReadOnly + { + get + { + return true; + } + } + + public bool Remove(KeyValuePair item) + { + throw new InvalidOperationException(RO_MSG); + } + + public IEnumerator> GetEnumerator() + { + foreach (KeyValuePair entry in this.map) + { + yield return new KeyValuePair( + entry.Key, entry.Value.SourcePath); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + } + + internal class TargetPathMap : IDictionary + { + private const string RO_MSG = + "The TargetPathMap collection is read-only. " + + "Modify the InstallPathMap instead."; + + private InstallPathMap map; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal TargetPathMap(InstallPathMap map) + { + this.map = map; + } + + public void Add(string key, string value) + { + throw new InvalidOperationException(RO_MSG); + } + + public bool ContainsKey(string key) + { + return this.map.ContainsKey(key); + } + + public ICollection Keys + { + get + { + return this.map.Keys; + } + } + + public bool Remove(string key) + { + throw new InvalidOperationException(RO_MSG); + } + + public bool TryGetValue(string key, out string value) + { + InstallPath installPath; + if (this.map.TryGetValue(key, out installPath)) + { + value = installPath.TargetPath; + return true; + } + else + { + value = null; + return false; + } + } + + public ICollection Values + { + get + { + List values = new List(this.Count); + foreach (KeyValuePair entry in this.map) + { + values.Add(entry.Value.TargetPath); + } + return values; + } + } + + public string this[string key] + { + get + { + string value = null; + this.TryGetValue(key, out value); + return value; + } + set + { + throw new InvalidOperationException(RO_MSG); + } + } + + public void Add(KeyValuePair item) + { + throw new InvalidOperationException(RO_MSG); + } + + public void Clear() + { + throw new InvalidOperationException(RO_MSG); + } + + public bool Contains(KeyValuePair item) + { + string value = this[item.Key]; + return value == item.Value; + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + foreach (KeyValuePair entry in this) + { + array[arrayIndex] = entry; + arrayIndex++; + } + } + + public int Count + { + get + { + return this.map.Count; + } + } + + public bool IsReadOnly + { + get + { + return true; + } + } + + public bool Remove(KeyValuePair item) + { + throw new InvalidOperationException(RO_MSG); + } + + public IEnumerator> GetEnumerator() + { + foreach (KeyValuePair entry in this.map) + { + yield return new KeyValuePair( + entry.Key, entry.Value.TargetPath); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Package/PatchPackage.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Package/PatchPackage.cs new file mode 100644 index 000000000..ef2b568f2 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Package/PatchPackage.cs @@ -0,0 +1,263 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller.Package +{ + using System; + using System.Collections; + using System.IO; + + /// + /// Provides access to convenient properties and operations on a patch package (.MSP). + /// + internal class PatchPackage : Database + { + /// + /// Creates a new patch package object; opening the patch database in read-only mode. + /// + /// Path to the patch package (.MSP) + /// The PatchPackage object only opens the patch database in read-only mode, because + /// transforms (sub-storages) cannot be read if the database is open in read-write mode. + public PatchPackage(string packagePath) + : base(packagePath, (DatabaseOpenMode) ((int) DatabaseOpenMode.ReadOnly | 32)) + // TODO: figure out what to do about DatabaseOpenMode.Patch + { + } + + /// + /// Handle this event to receive status messages when operations are performed on the patch package. + /// + /// + /// patchPackage.Message += new InstallPackageMessageHandler(Console.WriteLine); + /// + public event InstallPackageMessageHandler Message; + + /// + /// Sends a message to the event-handler. + /// + /// Message string, containing 0 or more format items + /// Items to be formatted + protected void LogMessage(string format, params object[] args) + { + if(this.Message != null) + { + this.Message(format, args); + } + } + + /// + /// Gets the patch code (GUID) of the patch package. + /// + /// + /// The patch code is stored in the RevisionNumber field of the patch summary information. + /// + public string PatchCode + { + get + { + string guids = this.SummaryInfo.RevisionNumber; + return guids.Substring(0, guids.IndexOf('}') + 1); + } + } + + /// + /// Gets the list of patch codes that are replaced by this patch package. + /// + /// Array of replaced patch codes (GUIDs) + /// + /// The list of replaced patch codes is stored in the RevisionNumber field of the patch summary information. + /// + public string[] GetReplacedPatchCodes() + { + ArrayList patchCodeList = new ArrayList(); + string guids = this.SummaryInfo.RevisionNumber; + int thisGuid = guids.IndexOf('}') + 1; + int nextGuid = guids.IndexOf('}', thisGuid) + 1; + while(nextGuid > 0) + { + patchCodeList.Add(guids.Substring(thisGuid, (nextGuid - thisGuid))); + thisGuid = nextGuid; + nextGuid = guids.IndexOf('}', thisGuid) + 1; + } + return (string[]) patchCodeList.ToArray(typeof(string)); + } + + /// + /// Gets the list of product codes of products targeted by this patch package. + /// + /// Array of product codes (GUIDs) + /// + /// The list of target product codes is stored in the Template field of the patch summary information. + /// + public string[] GetTargetProductCodes() + { + string productList = this.SummaryInfo.Template; + return productList.Split(';'); + } + + /// + /// Gets the names of the transforms included in the patch package. + /// + /// Array of transform names + /// + /// The returned list does not include the "patch special transforms" that are prefixed with "#" + ///

The list of transform names is stored in the LastSavedBy field of the patch summary information.

+ ///
+ public string[] GetTransforms() + { + return this.GetTransforms(false); + } + /// + /// Gets the names of the transforms included in the patch package. + /// + /// Specifies whether to include the + /// "patch special transforms" that are prefixed with "#" + /// Array of transform names + /// + /// The list of transform names is stored in the LastSavedBy field of the patch summary information. + /// + public string[] GetTransforms(bool includeSpecialTransforms) + { + ArrayList transformArray = new ArrayList(); + string transformList = this.SummaryInfo.LastSavedBy; + foreach(string transform in transformList.Split(';', ':')) + { + if(transform.Length != 0 && (includeSpecialTransforms || !transform.StartsWith("#", StringComparison.Ordinal))) + { + transformArray.Add(transform); + } + } + return (string[]) transformArray.ToArray(typeof(string)); + } + + /// + /// Gets information about the transforms included in the patch package. + /// + /// Array containing information about each transform + /// + /// The returned info does not include the "patch special transforms" that are prefixed with "#" + /// + public TransformInfo[] GetTransformsInfo() + { + return this.GetTransformsInfo(false); + } + + /// + /// Gets information about the transforms included in the patch package. + /// + /// Specifies whether to include the + /// "patch special transforms" that are prefixed with "#" + /// Array containing information about each transform + public TransformInfo[] GetTransformsInfo(bool includeSpecialTransforms) + { + string[] transforms = this.GetTransforms(includeSpecialTransforms); + ArrayList transformInfoArray = new ArrayList(transforms.Length); + foreach(string transform in transforms) + { + transformInfoArray.Add(this.GetTransformInfo(transform)); + } + return (TransformInfo[]) transformInfoArray.ToArray(typeof(TransformInfo)); + } + + /// + /// Gets information about a transforms included in the patch package. + /// + /// Name of the transform to extract; this may optionally be a + /// special transform prefixed by "#" + /// Information about the transform + public TransformInfo GetTransformInfo(string transform) + { + string tempTransformFile = null; + try + { + tempTransformFile = Path.GetTempFileName(); + this.ExtractTransform(transform, tempTransformFile); + using(SummaryInfo transformSummInfo = new SummaryInfo(tempTransformFile, false)) + { + return new TransformInfo(transform, transformSummInfo); + } + } + finally + { + if(tempTransformFile != null && File.Exists(tempTransformFile)) + { + File.Delete(tempTransformFile); + } + } + } + + /// + /// Analyzes the transforms included in the patch package to find the ones that + /// are applicable to an install package. + /// + /// The install package to validate the transforms against + /// Array of valid transform names + /// + /// The returned list does not include the "patch special transforms" that + /// are prefixed with "#" If a transform is valid, then its corresponding + /// special transform is assumed to be valid as well. + /// + public string[] GetValidTransforms(Database installPackage) + { + ArrayList transformArray = new ArrayList(); + string transformList = this.SummaryInfo.LastSavedBy; + foreach(string transform in transformList.Split(';', ':')) + { + if(transform.Length != 0 && !transform.StartsWith("#", StringComparison.Ordinal)) + { + this.LogMessage("Checking validity of transform {0}", transform); + string tempTransformFile = null; + try + { + tempTransformFile = Path.GetTempFileName(); + this.ExtractTransform(transform, tempTransformFile); + if(installPackage.IsTransformValid(tempTransformFile)) + { + this.LogMessage("Found valid transform: {0}", transform); + transformArray.Add(transform); + } + } + finally + { + if(tempTransformFile != null && File.Exists(tempTransformFile)) + { + try { File.Delete(tempTransformFile); } + catch(IOException) { } + } + } + } + } + return (string[]) transformArray.ToArray(typeof(string)); + } + + /// + /// Extracts a transform (.MST) from a patch package. + /// + /// Name of the transform to extract; this may optionally be a + /// special transform prefixed by "#" + /// Location where the transform will be extracted + public void ExtractTransform(string transform, string extractFile) + { + using(View stgView = this.OpenView("SELECT `Name`, `Data` FROM `_Storages` WHERE `Name` = '{0}'", transform)) + { + stgView.Execute(); + Record stgRec = stgView.Fetch(); + if(stgRec == null) + { + this.LogMessage("Transform not found: {0}", transform); + throw new InstallerException("Transform not found: " + transform); + } + using(stgRec) + { + stgRec.GetStream("Data", extractFile); + } + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Package/TransformInfo.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Package/TransformInfo.cs new file mode 100644 index 000000000..42ea699a6 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller.Package/TransformInfo.cs @@ -0,0 +1,161 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller.Package +{ + using System; + using System.Globalization; + using System.IO; + + /// + /// Contains properties of a transform package (.MST). + /// + internal class TransformInfo + { + /// + /// Reads transform information from a transform package. + /// + /// Path to a transform package (.MST file). + public TransformInfo(string mstFile) + { + this.name = Path.GetFileName(mstFile); + using (SummaryInfo transformSummInfo = new SummaryInfo(mstFile, false)) + { + this.DecodeSummaryInfo(transformSummInfo); + } + } + + /// + /// Reads transform information from the summary information of a transform package. + /// + /// Filename of the transform (optional). + /// Handle to the summary information of a transform package (.MST file). + public TransformInfo(string name, SummaryInfo transformSummaryInfo) + { + this.name = name; + this.DecodeSummaryInfo(transformSummaryInfo); + } + + private void DecodeSummaryInfo(SummaryInfo transformSummaryInfo) + { + try + { + string[] rev = transformSummaryInfo.RevisionNumber.Split(new char[] { ';' }, 3); + this.targetProductCode = rev[0].Substring(0, 38); + this.targetProductVersion = rev[0].Substring(38); + this.upgradeProductCode = rev[1].Substring(0, 38); + this.upgradeProductVersion = rev[1].Substring(38); + this.upgradeCode = rev[2]; + + string[] templ = transformSummaryInfo.Template.Split(new Char[] { ';' }, 2); + this.targetPlatform = templ[0]; + this.targetLanguage = 0; + if (templ.Length >= 2 && templ[1].Length > 0) + { + this.targetLanguage = Int32.Parse(templ[1], CultureInfo.InvariantCulture.NumberFormat); + } + + this.validateFlags = (TransformValidations) transformSummaryInfo.CharacterCount; + } + catch (Exception ex) + { + throw new InstallerException("Invalid transform summary info", ex); + } + } + + /// + /// Gets the filename of the transform. + /// + public string Name + { + get { return this.name; } + } + private string name; + + /// + /// Gets the target product code of the transform. + /// + public string TargetProductCode + { + get { return this.targetProductCode; } + } + private string targetProductCode; + + /// + /// Gets the target product version of the transform. + /// + public string TargetProductVersion + { + get { return this.targetProductVersion; } + } + private string targetProductVersion; + + /// + /// Gets the upgrade product code of the transform. + /// + public string UpgradeProductCode + { + get { return this.upgradeProductCode; } + } + private string upgradeProductCode; + + /// + /// Gets the upgrade product version of the transform. + /// + public string UpgradeProductVersion + { + get { return this.upgradeProductVersion; } + } + private string upgradeProductVersion; + + /// + /// Gets the upgrade code of the transform. + /// + public string UpgradeCode + { + get { return this.upgradeCode; } + } + private string upgradeCode; + + /// + /// Gets the target platform of the transform. + /// + public string TargetPlatform + { + get { return this.targetPlatform; } + } + private string targetPlatform; + + /// + /// Gets the target language of the transform, or 0 if the transform is language-neutral. + /// + public int TargetLanguage + { + get { return this.targetLanguage; } + } + private int targetLanguage; + + /// + /// Gets the validation flags specified when the transform was generated. + /// + public TransformValidations Validations + { + get { return this.validateFlags; } + } + private TransformValidations validateFlags; + + /// + /// Returns the name of the transform. + /// + public override string ToString() + { + return (this.Name != null ? this.Name : "MST"); + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ColumnCollection.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ColumnCollection.cs new file mode 100644 index 000000000..f38488ad6 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ColumnCollection.cs @@ -0,0 +1,343 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Text; + + /// + /// Collection of column information related to a or + /// . + /// + internal sealed class ColumnCollection : ICollection + { + private IList columns; + private string formatString; + + /// + /// Creates a new ColumnCollection based on a specified list of columns. + /// + /// columns to be added to the new collection + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public ColumnCollection(ICollection columns) + { + if (columns == null) + { + throw new ArgumentNullException("columns"); + } + + this.columns = new List(columns); + } + + /// + /// Creates a new ColumnCollection that is associated with a database table. + /// + /// view that contains the columns + internal ColumnCollection(View view) + { + if (view == null) + { + throw new ArgumentNullException("view"); + } + + this.columns = ColumnCollection.GetViewColumns(view); + } + + /// + /// Gets the number of columns in the collection. + /// + /// number of columns in the collection + public int Count + { + get + { + return this.columns.Count; + } + } + + /// + /// Gets a boolean value indicating whether the collection is read-only. + /// A ColumnCollection is read-only if it is associated with a + /// or a read-only . + /// + /// read-only status of the collection + public bool IsReadOnly + { + get + { + return true; + } + } + + /// + /// Gets information about a specific column in the collection. + /// + /// 1-based index into the column collection + /// is less + /// than 1 or greater than the number of columns in the collection + public ColumnInfo this[int columnIndex] + { + get + { + if (columnIndex >= 0 && columnIndex < this.columns.Count) + { + return this.columns[columnIndex]; + } + else + { + throw new ArgumentOutOfRangeException("columnIndex"); + } + } + } + + /// + /// Gets information about a specific column in the collection. + /// + /// case-sensitive name of a column collection + /// does + /// not exist in the collection + public ColumnInfo this[string columnName] + { + get + { + if (string.IsNullOrWhiteSpace(columnName)) + { + throw new ArgumentNullException("columnName"); + } + + foreach (ColumnInfo colInfo in this.columns) + { + if (colInfo.Name == columnName) + { + return colInfo; + } + } + + throw new ArgumentOutOfRangeException("columnName"); + } + } + + /// + /// Not supported because the collection is read-only. + /// + /// information about the column being added + /// the collection is read-only + public void Add(ColumnInfo item) + { + throw new InvalidOperationException(); + } + + /// + /// Not supported because the collection is read-only. + /// + /// the collection is read-only + public void Clear() + { + throw new InvalidOperationException(); + } + + /// + /// Checks if a column with a given name exists in the collection. + /// + /// case-sensitive name of the column to look for + /// true if the column exists in the collection, false otherwise + public bool Contains(string columnName) + { + return this.IndexOf(columnName) >= 0; + } + + /// + /// Checks if a column with a given name exists in the collection. + /// + /// column to look for, with case-sensitive name + /// true if the column exists in the collection, false otherwise + bool ICollection.Contains(ColumnInfo column) + { + return this.Contains(column.Name); + } + + /// + /// Gets the index of a column within the collection. + /// + /// case-sensitive name of the column to look for + /// 0-based index of the column, or -1 if not found + public int IndexOf(string columnName) + { + if (string.IsNullOrWhiteSpace(columnName)) + { + throw new ArgumentNullException("columnName"); + } + + for (int index = 0; index < this.columns.Count; index++) + { + if (this.columns[index].Name == columnName) + { + return index; + } + } + return -1; + } + + /// + /// Copies the columns from this collection into an array. + /// + /// destination array to be filed + /// offset into the destination array where copying begins + public void CopyTo(ColumnInfo[] array, int arrayIndex) + { + if (array == null) + { + throw new ArgumentNullException("array"); + } + + this.columns.CopyTo(array, arrayIndex); + } + + /// + /// Not supported because the collection is read-only. + /// + /// column to remove + /// true if the column was removed, false if it was not found + /// the collection is read-only + [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "column")] + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] + bool ICollection.Remove(ColumnInfo column) + { + throw new InvalidOperationException(); + } + + /// + /// Gets an enumerator over the columns in the collection. + /// + /// An enumerator of ColumnInfo objects. + public IEnumerator GetEnumerator() + { + return this.columns.GetEnumerator(); + } + + /// + /// Gets a string suitable for printing all the values of a record containing these columns. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string FormatString + { + get + { + if (this.formatString == null) + { + this.formatString = CreateFormatString(this.columns); + } + return this.formatString; + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + private static string CreateFormatString(IList columns) + { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < columns.Count; i++) + { + if (columns[i].Type == typeof(Stream)) + { + sb.AppendFormat("{0} = [Binary Data]", columns[i].Name); + } + else + { + sb.AppendFormat("{0} = [{1}]", columns[i].Name, i + 1); + } + + if (i < columns.Count - 1) + { + sb.Append(", "); + } + } + return sb.ToString(); + } + + /// + /// Gets an enumerator over the columns in the collection. + /// + /// An enumerator of ColumnInfo objects. + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + /// + /// Creates ColumnInfo objects for the associated view. + /// + /// dynamically-generated list of columns + private static IList GetViewColumns(View view) + { + IList columnNames = ColumnCollection.GetViewColumns(view, false); + IList columnTypes = ColumnCollection.GetViewColumns(view, true); + + int count = columnNames.Count; + if (columnTypes[count - 1] == "O0") + { + // Weird.. the "_Tables" table returns a second column with type "O0" -- ignore it. + count--; + } + + IList columnsList = new List(count); + for (int i = 0; i < count; i++) + { + columnsList.Add(new ColumnInfo(columnNames[i], columnTypes[i])); + } + + return columnsList; + } + + /// + /// Gets a list of column names or column-definition-strings for the + /// associated view. + /// + /// the view to that defines the columns + /// true to return types (column definition strings), + /// false to return names + /// list of column names or types + private static IList GetViewColumns(View view, bool types) + { + int recordHandle; + int typesFlag = types ? 1 : 0; + uint ret = RemotableNativeMethods.MsiViewGetColumnInfo( + (int) view.Handle, (uint) typesFlag, out recordHandle); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + using (Record rec = new Record((IntPtr) recordHandle, true, null)) + { + int count = rec.FieldCount; + IList columnsList = new List(count); + + // Since we must be getting all strings of limited length, + // this code is faster than calling rec.GetString(field). + for (int field = 1; field <= count; field++) + { + uint bufSize = 256; + StringBuilder buf = new StringBuilder((int) bufSize); + ret = RemotableNativeMethods.MsiRecordGetString((int) rec.Handle, (uint) field, buf, ref bufSize); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + columnsList.Add(buf.ToString()); + } + return columnsList; + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ColumnEnums.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ColumnEnums.cs new file mode 100644 index 000000000..43ec886c3 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ColumnEnums.cs @@ -0,0 +1,696 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Diagnostics.CodeAnalysis; + + // Enumerations are in alphabetical order. + + /// + /// Available values for the Attributes column of the Component table. + /// + [Flags] + public enum ComponentAttributes : int + { + /// + /// Local only - Component cannot be run from source. + /// + ///

+ /// Set this value for all components belonging to a feature to prevent the feature from being run-from-network or + /// run-from-source. Note that if a feature has no components, the feature always shows run-from-source and + /// run-from-my-computer as valid options. + ///

+ None = 0x0000, + + /// + /// Component can only be run from source. + /// + ///

+ /// Set this bit for all components belonging to a feature to prevent the feature from being run-from-my-computer. + /// Note that if a feature has no components, the feature always shows run-from-source and run-from-my-computer + /// as valid options. + ///

+ SourceOnly = 0x0001, + + /// + /// Component can run locally or from source. + /// + Optional = 0x0002, + + /// + /// If this bit is set, the value in the KeyPath column is used as a key into the Registry table. + /// + ///

+ /// If the Value field of the corresponding record in the Registry table is null, the Name field in that record + /// must not contain "+", "-", or "*". For more information, see the description of the Name field in Registry + /// table. + ///

Setting this bit is recommended for registry entries written to the HKCU hive. This ensures the installer + /// writes the necessary HKCU registry entries when there are multiple users on the same machine.

+ ///

+ RegistryKeyPath = 0x0004, + + /// + /// If this bit is set, the installer increments the reference count in the shared DLL registry of the component's + /// key file. If this bit is not set, the installer increments the reference count only if the reference count + /// already exists. + /// + SharedDllRefCount = 0x0008, + + /// + /// If this bit is set, the installer does not remove the component during an uninstall. The installer registers + /// an extra system client for the component in the Windows Installer registry settings. + /// + Permanent = 0x0010, + + /// + /// If this bit is set, the value in the KeyPath column is a key into the ODBCDataSource table. + /// + OdbcDataSource = 0x0020, + + /// + /// If this bit is set, the installer reevaluates the value of the statement in the Condition column upon a reinstall. + /// If the value was previously False and has changed to true, the installer installs the component. If the value + /// was previously true and has changed to false, the installer removes the component even if the component has + /// other products as clients. + /// + Transitive = 0x0040, + + /// + /// If this bit is set, the installer does not install or reinstall the component if a key path file or a key path + /// registry entry for the component already exists. The application does register itself as a client of the component. + /// + ///

+ /// Use this flag only for components that are being registered by the Registry table. Do not use this flag for + /// components registered by the AppId, Class, Extension, ProgId, MIME, and Verb tables. + ///

+ NeverOverwrite = 0x0080, + + /// + /// Set this bit to mark this as a 64-bit component. This attribute facilitates the installation of packages that + /// include both 32-bit and 64-bit components. If this bit is not set, the component is registered as a 32-bit component. + /// + ///

+ /// If this is a 64-bit component replacing a 32-bit component, set this bit and assign a new GUID in the + /// ComponentId column. + ///

+ SixtyFourBit = 0x0100, + + /// + /// Set this bit to disable registry reflection on all existing and new registry keys affected by this component. + /// + ///

+ /// If this bit is set, the Windows Installer calls the RegDisableReflectionKey on each key being accessed by the component. + /// This bit is available with Windows Installer version 4.0 and is ignored on 32-bit systems. + ///

+ DisableRegistryReflection = 0x0200, + + /// + /// [MSI 4.5] Set this bit for a component in a patch package to prevent leaving orphan components on the computer. + /// + ///

+ /// If a subsequent patch is installed, marked with the SupersedeEarlier flag in its MsiPatchSequence + /// table to supersede the first patch, Windows Installer 4.5 can unregister and uninstall components marked with the + /// UninstallOnSupersedence value. If the component is not marked with this bit, installation of a superseding patch can leave + /// behind an unused component on the computer. + ///

+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Supersedence")] + UninstallOnSupersedence = 0x0400, + + /// + /// [MSI 4.5] If a component is marked with this attribute value in at least one package installed on the system, + /// the installer treats the component as marked in all packages. If a package that shares the marked component + /// is uninstalled, Windows Installer 4.5 can continue to share the highest version of the component on the system, + /// even if that highest version was installed by the package that is being uninstalled. + /// + Shared = 0x0800, + } + + /// + /// Defines flags for the Attributes column of the Control table. + /// + [Flags] + public enum ControlAttributes : int + { + /// If this bit is set, the control is visible on the dialog box. + Visible = 0x00000001, + + /// specifies if the given control is enabled or disabled. Most controls appear gray when disabled. + Enabled = 0x00000002, + + /// If this bit is set, the control is displayed with a sunken, three dimensional look. + Sunken = 0x00000004, + + /// The Indirect control attribute specifies whether the value displayed or changed by this control is referenced indirectly. + Indirect = 0x00000008, + + /// If this bit is set on a control, the associated property specified in the Property column of the Control table is an integer. + Integer = 0x00000010, + + /// If this bit is set the text in the control is displayed in a right-to-left reading order. + RightToLeftReadingOrder = 0x00000020, + + /// If this style bit is set, text in the control is aligned to the right. + RightAligned = 0x00000040, + + /// If this bit is set, the scroll bar is located on the left side of the control, otherwise it is on the right. + LeftScroll = 0x00000080, + + /// This is a combination of the RightToLeftReadingOrder, RightAligned, and LeftScroll attributes. + Bidirectional = RightToLeftReadingOrder | RightAligned | LeftScroll, + + /// If this bit is set on a text control, the control is displayed transparently with the background showing through the control where there are no characters. + Transparent = 0x00010000, + + /// If this bit is set on a text control, the occurrence of the character "&" in a text string is displayed as itself. + NoPrefix = 0x00020000, + + /// If this bit is set the text in the control is displayed on a single line. + NoWrap = 0x00040000, + + /// If this bit is set for a text control, the control will automatically attempt to format the displayed text as a number representing a count of bytes. + FormatSize = 0x00080000, + + /// If this bit is set, fonts are created using the user's default UI code page. Otherwise it is created using the database code page. + UsersLanguage = 0x00100000, + + /// If this bit is set on an Edit control, the installer creates a multiple line edit control with a vertical scroll bar. + Multiline = 0x00010000, + + /// This attribute creates an edit control for entering passwords. The control displays each character as an asterisk (*) as they are typed into the control. + PasswordInput = 0x00200000, + + /// If this bit is set on a ProgressBar control, the bar is drawn as a series of small rectangles in Microsoft Windows 95-style. Otherwise it is drawn as a single continuous rectangle. + Progress95 = 0x00010000, + + /// If this bit is set, the control shows removable volumes. + RemovableVolume = 0x00010000, + + /// If this bit is set, the control shows fixed internal hard drives. + FixedVolume = 0x00020000, + + /// If this bit is set, the control shows remote volumes. + RemoteVolume = 0x00040000, + + /// If this bit is set, the control shows CD-ROM volumes. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Cdrom")] + CdromVolume = 0x00080000, + + /// If this bit is set, the control shows RAM disk volumes. + RamDiskVolume = 0x00100000, + + /// If this bit is set, the control shows floppy volumes. + FloppyVolume = 0x00200000, + + /// Specifies whether or not the rollback backup files are included in the costs displayed by the VolumeCostList control. + ShowRollbackCost = 0x00400000, + + /// If this bit is set, the items listed in the control are displayed in a specified order. Otherwise, items are displayed in alphabetical order. + Sorted = 0x00010000, + + /// If this bit is set on a combo box, the edit field is replaced by a static text field. This prevents a user from entering a new value and requires the user to choose only one of the predefined values. + ComboList = 0x00020000, + + //ImageHandle = 0x00010000, + + /// If this bit is set on a check box or a radio button group, the button is drawn with the appearance of a push button, but its logic stays the same. + PushLike = 0x00020000, + + /// If this bit is set, the text in the control is replaced by a bitmap image. The Text column in the Control table is a foreign key into the Binary table. + Bitmap = 0x00040000, + + /// If this bit is set, text is replaced by an icon image and the Text column in the Control table is a foreign key into the Binary table. + Icon = 0x00080000, + + /// If this bit is set, the picture is cropped or centered in the control without changing its shape or size. + FixedSize = 0x00100000, + + /// Specifies which size of the icon image to load. If none of the bits are set, the first image is loaded. + IconSize16 = 0x00200000, + + /// Specifies which size of the icon image to load. If none of the bits are set, the first image is loaded. + IconSize32 = 0x00400000, + + /// Specifies which size of the icon image to load. If none of the bits are set, the first image is loaded. + IconSize48 = 0x00600000, + + /// If this bit is set, and the installation is not yet running with elevated privileges, the control is created with a UAC icon. + ElevationShield = 0x00800000, + + /// If this bit is set, the RadioButtonGroup has text and a border displayed around it. + HasBorder = 0x01000000, + } + + /// + /// Defines flags for the Type column of the CustomAction table. + /// + [SuppressMessage("Microsoft.Usage", "CA2217:DoNotMarkEnumsWithFlags")] + [Flags] + public enum CustomActionTypes : int + { + /// Unspecified custom action type. + None = 0x0000, + + /// Target = entry point name + Dll = 0x0001, + + /// Target = command line args + Exe = 0x0002, + + /// Target = text string to be formatted and set into property + TextData = 0x0003, + + /// Target = entry point name, null if none to call + JScript = 0x0005, + + /// Target = entry point name, null if none to call + VBScript = 0x0006, + + /// Target = property list for nested engine initialization + Install = 0x0007, + + /// Source = File.File, file part of installation + SourceFile = 0x0010, + + /// Source = Directory.Directory, folder containing existing file + Directory = 0x0020, + + /// Source = Property.Property, full path to executable + Property = 0x0030, + + /// Ignore action return status, continue running + Continue = 0x0040, + + /// Run asynchronously + Async = 0x0080, + + /// Skip if UI sequence already run + FirstSequence = 0x0100, + + /// Skip if UI sequence already run in same process + OncePerProcess = 0x0200, + + /// Run on client only if UI already run on client + ClientRepeat = 0x0300, + + /// Queue for execution within script + InScript = 0x0400, + + /// In conjunction with InScript: queue in Rollback script + Rollback = 0x0100, + + /// In conjunction with InScript: run Commit ops from script on success + Commit = 0x0200, + + /// No impersonation, run in system context + NoImpersonate = 0x0800, + + /// Impersonate for per-machine installs on TS machines + TSAware = 0x4000, + + /// Script requires 64bit process + SixtyFourBitScript = 0x1000, + + /// Don't record the contents of the Target field in the log file + HideTarget = 0x2000, + + /// The custom action runs only when a patch is being uninstalled + PatchUninstall = 0x8000, + } + + /// + /// Defines flags for the Attributes column of the Dialog table. + /// + [Flags] + public enum DialogAttributes : int + { + /// If this bit is set, the dialog is originally created as visible, otherwise it is hidden. + Visible = 0x00000001, + + /// If this bit is set, the dialog box is modal, other dialogs of the same application cannot be put on top of it, and the dialog keeps the control while it is running. + Modal = 0x00000002, + + /// If this bit is set, the dialog box can be minimized. This bit is ignored for modal dialog boxes, which cannot be minimized. + Minimize = 0x00000004, + + /// If this style bit is set, the dialog box will stop all other applications and no other applications can take the focus. + SysModal = 0x00000008, + + /// If this bit is set, the other dialogs stay alive when this dialog box is created. + KeepModeless = 0x00000010, + + /// If this bit is set, the dialog box periodically calls the installer. If the property changes, it notifies the controls on the dialog. + TrackDiskSpace = 0x00000020, + + /// If this bit is set, the pictures on the dialog box are created with the custom palette (one per dialog received from the first control created). + UseCustomPalette = 0x00000040, + + /// If this style bit is set the text in the dialog box is displayed in right-to-left-reading order. + RightToLeftReadingOrder = 0x00000080, + + /// If this style bit is set, the text is aligned on the right side of the dialog box. + RightAligned = 0x00000100, + + /// If this style bit is set, the scroll bar is located on the left side of the dialog box. + LeftScroll = 0x00000200, + + /// This is a combination of the RightToLeftReadingOrder, RightAligned, and the LeftScroll dialog style bits. + Bidirectional = RightToLeftReadingOrder | RightAligned | LeftScroll, + + /// If this bit is set, the dialog box is an error dialog. + Error = 0x00010000, + } + + /// + /// Available values for the Attributes column of the Feature table. + /// + [Flags] + public enum FeatureAttributes : int + { + /// + /// Favor local - Components of this feature that are not marked for installation from source are installed locally. + /// + ///

+ /// A component shared by two or more features, some of which are set to FavorLocal and some to FavorSource, + /// is installed locally. Components marked in the Component + /// table are always run from the source CD/server. The bits FavorLocal and FavorSource work with features not + /// listed by the ADVERTISE property. + ///

+ None = 0x0000, + + /// + /// Components of this feature not marked for local installation are installed to run from the source + /// CD-ROM or server. + /// + ///

+ /// A component shared by two or more features, some of which are set to FavorLocal and some to FavorSource, + /// is installed to run locally. Components marked (local-only) in the + /// Component table are always installed locally. The bits FavorLocal and FavorSource work with features + /// not listed by the ADVERTISE property. + ///

+ FavorSource = 0x0001, + + /// + /// Set this attribute and the state of the feature is the same as the state of the feature's parent. + /// You cannot use this option if the feature is located at the root of a feature tree. + /// + ///

+ /// Omit this attribute and the feature state is determined according to DisallowAdvertise and + /// FavorLocal and FavorSource. + ///

To guarantee that the child feature's state always follows the state of its parent, even when the + /// child and parent are initially set to absent in the SelectionTree control, you must include both + /// FollowParent and UIDisallowAbsent in the attributes of the child feature.

+ ///

Note that if you set FollowParent without setting UIDisallowAbsent, the installer cannot force + /// the child feature out of the absent state. In this case, the child feature matches the parent's + /// installation state only if the child is set to something other than absent.

+ ///

Set FollowParent and UIDisallowAbsent to ensure a child feature follows the state of the parent feature.

+ ///

+ FollowParent = 0x0002, + + /// + /// Set this attribute and the feature state is Advertise. + /// + ///

+ /// If the feature is listed by the ADDDEFAULT property this bit is ignored and the feature state is determined + /// according to FavorLocal and FavorSource. + ///

Omit this attribute and the feature state is determined according to DisallowAdvertise and FavorLocal + /// and FavorSource.

+ ///

+ FavorAdvertise = 0x0004, + + /// + /// Set this attribute to prevent the feature from being advertised. + /// + ///

+ /// Note that this bit works only with features that are listed by the ADVERTISE property. + ///

Set this attribute and if the listed feature is not a parent or child, the feature is installed according to + /// FavorLocal and FavorSource.

+ ///

Set this attribute for the parent of a listed feature and the parent is installed.

+ ///

Set this attribute for the child of a listed feature and the state of the child is Absent.

+ ///

Omit this attribute and if the listed feature is not a parent or child, the feature state is Advertise.

+ ///

Omit this attribute and if the listed feature is a parent or child, the state of both features is Advertise.

+ ///

+ DisallowAdvertise = 0x0008, + + /// + /// Set this attribute and the user interface does not display an option to change the feature state + /// to Absent. Setting this attribute forces the feature to the installation state, whether or not the + /// feature is visible in the UI. + /// + ///

+ /// Omit this attribute and the user interface displays an option to change the feature state to Absent. + ///

Set FollowParent and UIDisallowAbsent to ensure a child feature follows the state of the parent feature.

+ ///

Setting this attribute not only affects the UI, but also forces the feature to the install state whether + /// the feature is visible in the UI or not.

+ ///

+ UIDisallowAbsent = 0x0010, + + /// + /// Set this attribute and advertising is disabled for the feature if the operating system shell does not + /// support Windows Installer descriptors. + /// + NoUnsupportedAdvertise = 0x0020, + } + + /// + /// Available values for the Attributes column of the File table. + /// + [Flags] + public enum FileAttributes : int + { + /// No attributes. + None = 0x0000, + + /// Read-only. + ReadOnly = 0x0001, + + /// Hidden. + Hidden = 0x0002, + + /// System. + System = 0x0004, + + /// The file is vital for the proper operation of the component to which it belongs. + Vital = 0x0200, + + /// The file contains a valid checksum. A checksum is required to repair a file that has become corrupted. + Checksum = 0x0400, + + /// This bit must only be added by a patch and if the file is being added by the patch. + PatchAdded = 0x1000, + + /// + /// The file's source type is uncompressed. If set, ignore the WordCount summary information property. If neither + /// Noncompressed nor Compressed are set, the compression state of the file is specified by the WordCount summary + /// information property. Do not set both Noncompressed and Compressed. + /// + NonCompressed = 0x2000, + + /// + /// The file's source type is compressed. If set, ignore the WordCount summary information property. If neither + /// Noncompressed or Compressed are set, the compression state of the file is specified by the WordCount summary + /// information property. Do not set both Noncompressed and Compressed. + /// + Compressed = 0x4000, + } + + /// + /// Defines values for the Action column of the IniFile and RemoveIniFile tables. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ini")] + public enum IniFileAction : int + { + /// Creates or updates a .ini entry. + AddLine = 0, + + /// Creates a .ini entry only if the entry does not already exist. + CreateLine = 1, + + /// Deletes .ini entry. + RemoveLine = 2, + + /// Creates a new entry or appends a new comma-separated value to an existing entry. + AddTag = 3, + + /// Deletes a tag from a .ini entry. + RemoveTag = 4, + } + + /// + /// Defines values for the Type column of the CompLocator, IniLocator, and RegLocator tables. + /// + [Flags] + [SuppressMessage("Microsoft.Design", "CA1008:EnumsShouldHaveZeroValue")] + public enum LocatorTypes : int + { + /// Key path is a directory. + Directory = 0x00000000, + + /// Key path is a file name. + FileName = 0x00000001, + + /// Key path is a registry value. + RawValue = 0x00000002, + + /// Set this bit to have the installer search the 64-bit portion of the registry. + SixtyFourBit = 0x00000010, + } + + /// + /// Defines values for the Root column of the Registry, RemoveRegistry, and RegLocator tables. + /// + public enum RegistryRoot : int + { + /// HKEY_CURRENT_USER for a per-user installation, + /// or HKEY_LOCAL_MACHINE for a per-machine installation. + UserOrMachine = -1, + + /// HKEY_CLASSES_ROOT + ClassesRoot = 0, + + /// HKEY_CURRENT_USER + CurrentUser = 1, + + /// HKEY_LOCAL_MACHINE + LocalMachine = 2, + + /// HKEY_USERS + Users = 3, + } + + /// + /// Defines values for the InstallMode column of the RemoveFile table. + /// + [Flags] + public enum RemoveFileModes : int + { + /// Never remove. + None = 0, + + /// Remove when the associated component is being installed (install state = local or source). + OnInstall = 1, + + /// Remove when the associated component is being removed (install state = absent). + OnRemove = 2, + } + + /// + /// Defines values for the ServiceType, StartType, and ErrorControl columns of the ServiceInstall table. + /// + [Flags] + public enum ServiceAttributes : int + { + /// No flags. + None = 0, + + /// A Win32 service that runs its own process. + OwnProcess = 0x0010, + + /// A Win32 service that shares a process. + ShareProcess = 0x0020, + + /// A Win32 service that interacts with the desktop. + /// This value cannot be used alone and must be added to either + /// or . + Interactive = 0x0100, + + /// Service starts during startup of the system. + AutoStart = 0x0002, + + /// Service starts when the service control manager calls the StartService function. + DemandStart = 0x0003, + + /// Specifies a service that can no longer be started. + Disabled = 0x0004, + + /// Logs the error, displays a message box and continues the startup operation. + ErrorMessage = 0x0001, + + /// Logs the error if it is possible and the system is restarted with the last configuration + /// known to be good. If the last-known-good configuration is being started, the startup operation fails. + ErrorCritical = 0x0003, + + /// When combined with other error flags, specifies that the overall install should fail if + /// the service cannot be installed into the system. + ErrorControlVital = 0x8000, + } + + /// + /// Defines values for the Event column of the ServiceControl table. + /// + [Flags] + public enum ServiceControlEvents : int + { + /// No control events. + None = 0x0000, + + /// During an install, starts the service during the StartServices action. + Start = 0x0001, + + /// During an install, stops the service during the StopServices action. + Stop = 0x0002, + + /// During an install, deletes the service during the DeleteServices action. + Delete = 0x0008, + + /// During an uninstall, starts the service during the StartServices action. + UninstallStart = 0x0010, + + /// During an uninstall, stops the service during the StopServices action. + UninstallStop = 0x0020, + + /// During an uninstall, deletes the service during the DeleteServices action. + UninstallDelete = 0x0080, + } + + /// + /// Defines values for the StyleBits column of the TextStyle table. + /// + [Flags] + public enum TextStyles : int + { + /// Bold + Bold = 0x0001, + + /// Italic + Italic = 0x0002, + + /// Underline + Underline = 0x0004, + + /// Strike out + Strike = 0x0008, + } + + /// + /// Defines values for the Attributes column of the Upgrade table. + /// + [Flags] + public enum UpgradeAttributes : int + { + /// Migrates feature states by enabling the logic in the MigrateFeatureStates action. + MigrateFeatures = 0x0001, + + /// Detects products and applications but does not remove. + OnlyDetect = 0x0002, + + /// Continues installation upon failure to remove a product or application. + IgnoreRemoveFailure = 0x0004, + + /// Detects the range of versions including the value in VersionMin. + VersionMinInclusive = 0x0100, + + /// Dectects the range of versions including the value in VersionMax. + VersionMaxInclusive = 0x0200, + + /// Detects all languages, excluding the languages listed in the Language column. + LanguagesExclusive = 0x0400, + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ColumnInfo.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ColumnInfo.cs new file mode 100644 index 000000000..0bd8c9b9b --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ColumnInfo.cs @@ -0,0 +1,308 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Text; + + /// + /// Defines a single column of a table in an installer database. + /// + /// Once created, a ColumnInfo object is immutable. + internal class ColumnInfo + { + private string name; + private Type type; + private int size; + private bool isRequired; + private bool isTemporary; + private bool isLocalizable; + + /// + /// Creates a new ColumnInfo object from a column definition. + /// + /// name of the column + /// column definition string + /// + public ColumnInfo(string name, string columnDefinition) + : this(name, typeof(String), 0, false, false, false) + { + if (name == null) + { + throw new ArgumentNullException("name"); + } + + if (columnDefinition == null) + { + throw new ArgumentNullException("columnDefinition"); + } + + switch (Char.ToLower(columnDefinition[0], CultureInfo.InvariantCulture)) + { + case 'i': this.type = typeof(Int32); + break; + case 'j': this.type = typeof(Int32); this.isTemporary = true; + break; + case 'g': this.type = typeof(String); this.isTemporary = true; + break; + case 'l': this.type = typeof(String); this.isLocalizable = true; + break; + case 's': this.type = typeof(String); + break; + case 'v': this.type = typeof(Stream); + break; + default: throw new InstallerException(); + } + + this.isRequired = Char.IsLower(columnDefinition[0]); + this.size = Int32.Parse( + columnDefinition.Substring(1), + CultureInfo.InvariantCulture.NumberFormat); + if (this.type == typeof(Int32) && this.size <= 2) + { + this.type = typeof(Int16); + } + } + + /// + /// Creates a new ColumnInfo object from a list of parameters. + /// + /// name of the column + /// type of the column; must be one of the following: + /// Int16, Int32, String, or Stream + /// the maximum number of characters for String columns; + /// ignored for other column types + /// true if the column is required to have a non-null value + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public ColumnInfo(string name, Type type, int size, bool isRequired) + : this(name, type, size, isRequired, false, false) + { + } + + /// + /// Creates a new ColumnInfo object from a list of parameters. + /// + /// name of the column + /// type of the column; must be one of the following: + /// Int16, Int32, String, or Stream + /// the maximum number of characters for String columns; + /// ignored for other column types + /// true if the column is required to have a non-null value + /// true to if the column is only in-memory and + /// not persisted with the database + /// for String columns, indicates the column + /// is localizable; ignored for other column types + public ColumnInfo(string name, Type type, int size, bool isRequired, bool isTemporary, bool isLocalizable) + { + if (name == null) + { + throw new ArgumentNullException("name"); + } + + if (type == typeof(Int32)) + { + size = 4; + isLocalizable = false; + } + else if (type == typeof(Int16)) + { + size = 2; + isLocalizable = false; + } + else if (type == typeof(String)) + { + } + else if (type == typeof(Stream)) + { + isLocalizable = false; + } + else + { + throw new ArgumentOutOfRangeException("type"); + } + + this.name = name; + this.type = type; + this.size = size; + this.isRequired = isRequired; + this.isTemporary = isTemporary; + this.isLocalizable = isLocalizable; + } + + /// + /// Gets the name of the column. + /// + /// name of the column + public string Name + { + get { return this.name; } + } + + /// + /// Gets the type of the column as a System.Type. This is one of the following: Int16, Int32, String, or Stream + /// + /// type of the column + [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods")] + public Type Type + { + get { return this.type; } + } + + /// + /// Gets the type of the column as an integer that can be cast to a System.Data.DbType. This is one of the following: Int16, Int32, String, or Binary + /// + /// equivalent DbType of the column as an integer + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public int DBType + { + get + { + if (this.type == typeof(Int16)) return 10; + else if (this.type == typeof(Int32)) return 11; + else if (this.type == typeof(Stream)) return 1; + else return 16; + } + } + + /// + /// Gets the size of the column. + /// + /// The size of integer columns this is either 2 or 4. For string columns this is the maximum + /// recommended length of the string, or 0 for unlimited length. For stream columns, 0 is returned. + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public int Size + { + get { return this.size; } + } + + /// + /// Gets a value indicating whether the column must be non-null when inserting a record. + /// + /// required status of the column + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool IsRequired + { + get { return this.isRequired; } + } + + /// + /// Gets a value indicating whether the column is temporary. Temporary columns are not persisted + /// when the database is saved to disk. + /// + /// temporary status of the column + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool IsTemporary + { + get { return this.isTemporary; } + } + + /// + /// Gets a value indicating whether the column is a string column that is localizable. + /// + /// localizable status of the column + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool IsLocalizable + { + get { return this.isLocalizable; } + } + + /// + /// Gets an SQL fragment that can be used to create this column within a CREATE TABLE statement. + /// + /// SQL fragment to be used for creating the column + ///

+ /// Examples: + /// + /// LONG + /// SHORT TEMPORARY + /// CHAR(0) LOCALIZABLE + /// CHAR(72) NOT NULL LOCALIZABLE + /// OBJECT + /// + ///

+ public string SqlCreateString + { + get + { + StringBuilder s = new StringBuilder(); + s.AppendFormat("`{0}` ", this.name); + if (this.type == typeof(Int16)) s.Append("SHORT"); + else if (this.type == typeof(Int32)) s.Append("LONG"); + else if (this.type == typeof(String)) s.AppendFormat("CHAR({0})", this.size); + else s.Append("OBJECT"); + if (this.isRequired) s.Append(" NOT NULL"); + if (this.isTemporary) s.Append(" TEMPORARY"); + if (this.isLocalizable) s.Append(" LOCALIZABLE"); + return s.ToString(); + } + } + + /// + /// Gets a short string defining the type and size of the column. + /// + /// + /// The definition string consists + /// of a single letter representing the data type followed by the width of the column (in characters + /// when applicable, bytes otherwise). A width of zero designates an unbounded width (for example, + /// long text fields and streams). An uppercase letter indicates that null values are allowed in + /// the column. + /// + ///

+ /// + /// s? - String, variable length (?=1-255) + /// s0 - String, variable length + /// i2 - Short integer + /// i4 - Long integer + /// v0 - Binary Stream + /// g? - Temporary string (?=0-255) + /// j? - Temporary integer (?=0,1,2,4) + /// l? - Localizable string, variable length (?=1-255) + /// l0 - Localizable string, variable length + /// + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string ColumnDefinitionString + { + get + { + char t; + if (this.type == typeof(Int16) || this.type == typeof(Int32)) + { + t = (this.isTemporary ? 'j' : 'i'); + } + else if (this.type == typeof(String)) + { + t = (this.isTemporary ? 'g' : this.isLocalizable ? 'l' : 's'); + } + else + { + t = 'v'; + } + return String.Format( + CultureInfo.InvariantCulture, + "{0}{1}", + (this.isRequired ? t : Char.ToUpper(t, CultureInfo.InvariantCulture)), + this.size); + } + } + + /// + /// Gets the name of the column. + /// + /// Name of the column. + public override string ToString() + { + return this.Name; + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ComponentInfo.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ComponentInfo.cs new file mode 100644 index 000000000..78cc82a04 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ComponentInfo.cs @@ -0,0 +1,290 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Text; + + /// + /// Accessor for information about components within the context of an installation session. + /// + internal sealed class ComponentInfoCollection : ICollection + { + private Session session; + + internal ComponentInfoCollection(Session session) + { + this.session = session; + } + + /// + /// Gets information about a component within the context of an installation session. + /// + /// name of the component + /// component object + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public ComponentInfo this[string component] + { + get + { + return new ComponentInfo(this.session, component); + } + } + + void ICollection.Add(ComponentInfo item) + { + throw new InvalidOperationException(); + } + + void ICollection.Clear() + { + throw new InvalidOperationException(); + } + + /// + /// Checks if the collection contains a component. + /// + /// name of the component + /// true if the component is in the collection, else false + public bool Contains(string component) + { + return this.session.Database.CountRows( + "Component", "`Component` = '" + component + "'") == 1; + } + + bool ICollection.Contains(ComponentInfo item) + { + return item != null && this.Contains(item.Name); + } + + /// + /// Copies the features into an array. + /// + /// array that receives the features + /// offset into the array + public void CopyTo(ComponentInfo[] array, int arrayIndex) + { + if (array == null) { + throw new ArgumentNullException("array"); + } + foreach (ComponentInfo component in this) + { + array[arrayIndex++] = component; + } + } + + /// + /// Gets the number of components defined for the product. + /// + public int Count + { + get + { + return this.session.Database.CountRows("Component"); + } + } + + bool ICollection.IsReadOnly + { + get + { + return true; + } + } + + bool ICollection.Remove(ComponentInfo item) + { + throw new InvalidOperationException(); + } + + /// + /// Enumerates the components in the collection. + /// + /// an enumerator over all features in the collection + public IEnumerator GetEnumerator() + { + using (View compView = this.session.Database.OpenView( + "SELECT `Component` FROM `Component`")) + { + compView.Execute(); + + foreach (Record compRec in compView) using (compRec) + { + string comp = compRec.GetString(1); + yield return new ComponentInfo(this.session, comp); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + } + + /// + /// Provides access to information about a component within the context of an installation session. + /// + internal class ComponentInfo + { + private Session session; + private string name; + + internal ComponentInfo(Session session, string name) + { + this.session = session; + this.name = name; + } + + /// + /// Gets the name of the component (primary key in the Component table). + /// + public string Name + { + get + { + return this.name; + } + } + + /// + /// Gets the current install state of the designated Component. + /// + /// the Session handle is invalid + /// an unknown Component was requested + ///

+ /// Win32 MSI API: + /// MsiGetComponentState + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public InstallState CurrentState + { + get + { + int installedState, actionState; + uint ret = RemotableNativeMethods.MsiGetComponentState((int) this.session.Handle, this.name, out installedState, out actionState); + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.UNKNOWN_COMPONENT) + { + throw InstallerException.ExceptionFromReturnCode(ret, this.name); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + return (InstallState) installedState; + } + } + + /// + /// Gets or sets the action state of the designated Component. + /// + /// the Session handle is invalid + /// an unknown Component was requested + /// the user exited the installation + ///

+ /// Win32 MSI APIs: + /// MsiGetComponentState, + /// MsiSetComponentState + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public InstallState RequestState + { + get + { + int installedState, actionState; + uint ret = RemotableNativeMethods.MsiGetComponentState((int) this.session.Handle, this.name, out installedState, out actionState); + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.UNKNOWN_COMPONENT) + { + throw InstallerException.ExceptionFromReturnCode(ret, this.name); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + return (InstallState) actionState; + } + + set + { + uint ret = RemotableNativeMethods.MsiSetComponentState((int) this.session.Handle, this.name, (int) value); + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.UNKNOWN_COMPONENT) + { + throw InstallerException.ExceptionFromReturnCode(ret, this.name); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + } + } + + /// + /// Gets disk space per drive required to install a component. + /// + /// Requested component state + /// A list of InstallCost structures, specifying the cost for each drive for the component + ///

+ /// Win32 MSI API: + /// MsiEnumComponentCosts + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public IList GetCost(InstallState installState) + { + IList costs = new List(); + StringBuilder driveBuf = new StringBuilder(20); + for (uint i = 0; true; i++) + { + int cost, tempCost; + uint driveBufSize = (uint) driveBuf.Capacity; + uint ret = RemotableNativeMethods.MsiEnumComponentCosts( + (int) this.session.Handle, + this.name, + i, + (int) installState, + driveBuf, + ref driveBufSize, + out cost, + out tempCost); + if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) break; + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + driveBuf.Capacity = (int) ++driveBufSize; + ret = RemotableNativeMethods.MsiEnumComponentCosts( + (int) this.session.Handle, + this.name, + i, + (int) installState, + driveBuf, + ref driveBufSize, + out cost, + out tempCost); + } + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + costs.Add(new InstallCost(driveBuf.ToString(), cost * 512L, tempCost * 512L)); + } + return costs; + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ComponentInstallation.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ComponentInstallation.cs new file mode 100644 index 000000000..792f3077b --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ComponentInstallation.cs @@ -0,0 +1,392 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Text; + + /// + /// Represents an instance of a registered component. + /// + internal class ComponentInstallation : InstallationPart + { + /// + /// Gets the set of installed components for all products. + /// + /// The installer configuration data is corrupt + ///

+ /// Win32 MSI API: + /// MsiEnumComponents + ///

+ public static IEnumerable AllComponents + { + get + { + StringBuilder buf = new StringBuilder(40); + for (uint i = 0; true; i++) + { + uint ret = NativeMethods.MsiEnumComponents(i, buf); + if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) break; + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + yield return new ComponentInstallation(buf.ToString()); + } + } + } + + /// + /// Gets the set of installed components for products in the indicated context. + /// + /// The installer configuration data is corrupt + ///

+ /// Win32 MSI API: + /// MsiEnumComponentsEx + ///

+ public static IEnumerable Components(string szUserSid, UserContexts dwContext) + { + uint pcchSid = 32; + StringBuilder szSid = new StringBuilder((int)pcchSid); + StringBuilder buf = new StringBuilder(40); + UserContexts installedContext; + for (uint i = 0; true; i++) + { + uint ret = NativeMethods.MsiEnumComponentsEx(szUserSid, dwContext, i, buf, out installedContext, szSid, ref pcchSid); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + szSid.EnsureCapacity((int) ++pcchSid); + ret = NativeMethods.MsiEnumComponentsEx(szUserSid, dwContext, i, buf, out installedContext, szSid, ref pcchSid); + } + if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) break; + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + yield return new ComponentInstallation(buf.ToString(), szSid.ToString(), installedContext); + } + } + private static string GetProductCode(string component) + { + StringBuilder buf = new StringBuilder(40); + uint ret = NativeMethods.MsiGetProductCode(component, buf); + if (ret != 0) + { + return null; + } + + return buf.ToString(); + } + + private static string GetProductCode(string component, string szUserSid, UserContexts dwContext) + { + // TODO: We really need what would be MsiGetProductCodeEx, or at least a reasonable facimile thereof (something that restricts the search to the context defined by szUserSid & dwContext) + return GetProductCode(component); + } + + /// + /// Creates a new ComponentInstallation, automatically detecting the + /// product that the component is a part of. + /// + /// component GUID + ///

+ /// Win32 MSI API: + /// MsiGetProductCode + ///

+ public ComponentInstallation(string componentCode) + : this(componentCode, ComponentInstallation.GetProductCode(componentCode)) + { + } + + /// + /// Creates a new ComponentInstallation, automatically detecting the + /// product that the component is a part of. + /// + /// component GUID + /// context user SID + /// user contexts + public ComponentInstallation(string componentCode, string szUserSid, UserContexts dwContext) + : this(componentCode, ComponentInstallation.GetProductCode(componentCode, szUserSid, dwContext), szUserSid, dwContext) + { + } + + /// + /// Creates a new ComponentInstallation for a component installed by a + /// specific product. + /// + /// component GUID + /// ProductCode GUID + public ComponentInstallation(string componentCode, string productCode) + : this(componentCode, productCode, null, UserContexts.None) + { + } + + /// + /// Creates a new ComponentInstallation for a component installed by a + /// specific product. + /// + /// component GUID + /// ProductCode GUID + /// context user SID + /// user contexts + public ComponentInstallation(string componentCode, string productCode, string szUserSid, UserContexts dwContext) + : base(componentCode, productCode, szUserSid, dwContext) + { + if (string.IsNullOrWhiteSpace(componentCode)) + { + throw new ArgumentNullException("componentCode"); + } + } + + /// + /// Gets the component code (GUID) of the component. + /// + public string ComponentCode + { + get + { + return this.Id; + } + } + + /// + /// Gets all client products of a specified component. + /// + /// enumeration over all client products of the component + /// The installer configuration data is corrupt + ///

+ /// Because clients are not ordered, any new component has an arbitrary index. + /// This means that the property may return clients in any order. + ///

+ /// Win32 MSI API: + /// MsiEnumClients, + /// MsiEnumClientsEx + ///

+ public IEnumerable ClientProducts + { + get + { + StringBuilder buf = new StringBuilder(40); + for (uint i = 0; true; i++) + { + uint chSid = 0; + UserContexts installedContext; + uint ret = this.Context == UserContexts.None ? + NativeMethods.MsiEnumClients(this.ComponentCode, i, buf) : + NativeMethods.MsiEnumClientsEx(this.ComponentCode, this.UserSid, this.Context, i, buf, out installedContext, null, ref chSid); + if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) break; + else if (ret == (uint) NativeMethods.Error.UNKNOWN_COMPONENT) break; + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + yield return new ProductInstallation(buf.ToString()); + } + } + } + + /// + /// Gets the installed state of a component. + /// + /// the installed state of the component, or InstallState.Unknown + /// if this component is not part of a product + ///

+ /// Win32 MSI API: + /// MsiGetComponentPath, + /// MsiGetComponentPathEx + ///

+ public override InstallState State + { + get + { + if (this.ProductCode != null) + { + uint bufSize = 0; + int installState = this.Context == UserContexts.None ? + NativeMethods.MsiGetComponentPath( + this.ProductCode, this.ComponentCode, null, ref bufSize) : + NativeMethods.MsiGetComponentPathEx( + this.ProductCode, this.ComponentCode, this.UserSid, this.Context, null, ref bufSize); + return (InstallState) installState; + } + else + { + return InstallState.Unknown; + } + } + } + + /// + /// Gets the full path to an installed component. If the key path for the component is a + /// registry key then the registry key is returned. + /// + /// The file or registry keypath to the component, or null if the component is not available. + /// An unknown product or component was specified + /// The installer configuration data is corrupt + ///

+ /// If the component is a registry key, the registry roots are represented numerically. + /// For example, a registry path of "HKEY_CURRENT_USER\SOFTWARE\Microsoft" would be returned + /// as "01:\SOFTWARE\Microsoft". The registry roots returned are defined as follows: + /// HKEY_CLASSES_ROOT=00, HKEY_CURRENT_USER=01, HKEY_LOCAL_MACHINE=02, HKEY_USERS=03, + /// HKEY_PERFORMANCE_DATA=04 + ///

+ /// Win32 MSI APIs: + /// MsiGetComponentPath, + /// MsiGetComponentPathEx, + /// MsiLocateComponent + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string Path + { + get + { + StringBuilder buf = new StringBuilder(256); + uint bufSize = (uint) buf.Capacity; + InstallState installState; + + if (this.ProductCode != null) + { + installState = (this.Context == UserContexts.None) ? + (InstallState) NativeMethods.MsiGetComponentPath( + this.ProductCode, this.ComponentCode, buf, ref bufSize) : + (InstallState) NativeMethods.MsiGetComponentPathEx( + this.ProductCode, this.ComponentCode, this.UserSid, this.Context, buf, ref bufSize); + if (installState == InstallState.MoreData) + { + buf.Capacity = (int) ++bufSize; + installState = (this.Context == UserContexts.None) ? + (InstallState) NativeMethods.MsiGetComponentPath( + this.ProductCode, this.ComponentCode, buf, ref bufSize) : + (InstallState) NativeMethods.MsiGetComponentPathEx( + this.ProductCode, this.ComponentCode, this.UserSid, this.Context, buf, ref bufSize); + } + } + else + { + installState = (InstallState) NativeMethods.MsiLocateComponent( + this.ComponentCode, buf, ref bufSize); + if (installState == InstallState.MoreData) + { + buf.Capacity = (int) ++bufSize; + installState = (InstallState) NativeMethods.MsiLocateComponent( + this.ComponentCode, buf, ref bufSize); + } + } + + switch (installState) + { + case InstallState.Local: + case InstallState.Source: + case InstallState.SourceAbsent: + return buf.ToString(); + + default: + return null; + } + } + } + + /// + /// Gets the set of registered qualifiers for the component. + /// + /// Enumeration of the qulifiers for the component. + /// The installer configuration data is corrupt + ///

+ /// Because qualifiers are not ordered, any new qualifier has an arbitrary index, + /// meaning the function can return qualifiers in any order. + ///

+ /// Win32 MSI API: + /// MsiEnumComponentQualifiers + ///

+ public IEnumerable Qualifiers + { + get + { + StringBuilder qualBuf = new StringBuilder(255); + StringBuilder dataBuf = new StringBuilder(255); + for (uint i = 0; ; i++) + { + uint qualBufSize = (uint) qualBuf.Capacity; + uint dataBufSize = (uint) dataBuf.Capacity; + uint ret = NativeMethods.MsiEnumComponentQualifiers( + this.ComponentCode, i, qualBuf, ref qualBufSize, dataBuf, ref dataBufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + qualBuf.Capacity = (int) ++qualBufSize; + dataBuf.Capacity = (int) ++dataBufSize; + ret = NativeMethods.MsiEnumComponentQualifiers( + this.ComponentCode, i, qualBuf, ref qualBufSize, dataBuf, ref dataBufSize); + } + + if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS || + ret == (uint) NativeMethods.Error.UNKNOWN_COMPONENT) + { + break; + } + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + yield return new ComponentInstallation.Qualifier( + qualBuf.ToString(), dataBuf.ToString()); + } + } + } + + /// + /// Holds data about a component qualifier. + /// + ///

+ /// Win32 MSI API: + /// MsiEnumComponentQualifiers + ///

+ [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")] + [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes")] + public struct Qualifier + { + private string qualifierCode; + private string data; + + internal Qualifier(string qualifierCode, string data) + { + this.qualifierCode = qualifierCode; + this.data = data; + } + + /// + /// Gets the qualifier code. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string QualifierCode + { + get + { + return this.qualifierCode; + } + } + + /// + /// Gets the qualifier data. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string Data + { + get + { + return this.data; + } + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/CustomActionAttribute.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/CustomActionAttribute.cs new file mode 100644 index 000000000..3e949d504 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/CustomActionAttribute.cs @@ -0,0 +1,61 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + + /// + /// Marks a method as a custom action entry point. + /// + ///

+ /// A custom action method must be defined as public and static, + /// take a single object as a parameter, + /// and return an enumeration value. + ///

+ [Serializable, AttributeUsage(AttributeTargets.Method)] + internal sealed class CustomActionAttribute : Attribute + { + /// + /// Name of the custom action entrypoint, or null if the same as the method name. + /// + private string name; + + /// + /// Marks a method as a custom action entry point. + /// + public CustomActionAttribute() + : this(null) + { + } + + /// + /// Marks a method as a custom action entry point. + /// + /// Name of the function to be exported, + /// defaults to the name of this method + public CustomActionAttribute(string name) + { + this.name = name; + } + + /// + /// Gets or sets the name of the custom action entrypoint. A null + /// value defaults to the name of the method. + /// + /// name of the custom action entrypoint, or null if none was specified + public string Name + { + get + { + return this.name; + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/CustomActionProxy.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/CustomActionProxy.cs new file mode 100644 index 000000000..bb947a2af --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/CustomActionProxy.cs @@ -0,0 +1,324 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections; + using System.Configuration; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Reflection; + using System.Runtime.InteropServices; + using System.Security; + + /// + /// Managed-code portion of the custom action proxy. + /// + internal static class CustomActionProxy + { + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static int InvokeCustomAction32(int sessionHandle, string entryPoint, + int remotingDelegatePtr) + { + return CustomActionProxy.InvokeCustomAction(sessionHandle, entryPoint, new IntPtr(remotingDelegatePtr)); + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static int InvokeCustomAction64(int sessionHandle, string entryPoint, + long remotingDelegatePtr) + { + return CustomActionProxy.InvokeCustomAction(sessionHandle, entryPoint, new IntPtr(remotingDelegatePtr)); + } + + /// + /// Invokes a managed custom action method. + /// + /// Integer handle to the installer session. + /// Name of the custom action entrypoint. This must + /// either map to an entrypoint definition in the customActions + /// config section, or be an explicit entrypoint of the form: + /// "AssemblyName!Namespace.Class.Method" + /// Pointer to a delegate used to + /// make remote API calls, if this custom action is running out-of-proc. + /// The value returned by the custom action method, + /// or ERROR_INSTALL_FAILURE if the custom action could not be invoked. + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static int InvokeCustomAction(int sessionHandle, string entryPoint, + IntPtr remotingDelegatePtr) + { + Session session = null; + string assemblyName, className, methodName; + MethodInfo method; + + try + { + MsiRemoteInvoke remotingDelegate = (MsiRemoteInvoke) + Marshal.GetDelegateForFunctionPointer( + remotingDelegatePtr, typeof(MsiRemoteInvoke)); + RemotableNativeMethods.RemotingDelegate = remotingDelegate; + + sessionHandle = RemotableNativeMethods.MakeRemoteHandle(sessionHandle); + session = new Session((IntPtr) sessionHandle, false); + if (string.IsNullOrWhiteSpace(entryPoint)) + { + throw new ArgumentNullException("entryPoint"); + } + + if (!CustomActionProxy.FindEntryPoint( + session, + entryPoint, + out assemblyName, + out className, + out methodName)) + { + return (int) ActionResult.Failure; + } + session.Log("Calling custom action {0}!{1}.{2}", assemblyName, className, methodName); + + method = CustomActionProxy.GetCustomActionMethod( + session, + assemblyName, + className, + methodName); + if (method == null) + { + return (int) ActionResult.Failure; + } + } + catch (Exception ex) + { + if (session != null) + { + try + { + session.Log("Exception while loading custom action:"); + session.Log(ex.ToString()); + } + catch (Exception) { } + } + return (int) ActionResult.Failure; + } + + try + { + // Set the current directory to the location of the extracted files. + Environment.CurrentDirectory = + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + + object[] args = new object[] { session }; + if (DebugBreakEnabled(new string[] { entryPoint, methodName })) + { + string message = String.Format(CultureInfo.InvariantCulture, + "To debug your custom action, attach to process ID {0} (0x{0:x}) and click OK; otherwise, click Cancel to fail the custom action.", + System.Diagnostics.Process.GetCurrentProcess().Id + ); + + MessageResult button = NativeMethods.MessageBox( + IntPtr.Zero, + message, + "Custom Action Breakpoint", + (int)MessageButtons.OKCancel | (int)MessageIcon.Asterisk | (int)(MessageBoxStyles.TopMost | MessageBoxStyles.ServiceNotification) + ); + + if (MessageResult.Cancel == button) + { + return (int)ActionResult.UserExit; + } + } + + ActionResult result = (ActionResult) method.Invoke(null, args); + session.Close(); + return (int) result; + } + catch (InstallCanceledException) + { + return (int) ActionResult.UserExit; + } + catch (Exception ex) + { + session.Log("Exception thrown by custom action:"); + session.Log(ex.ToString()); + return (int) ActionResult.Failure; + } + } + + /// + /// Checks the "MMsiBreak" environment variable for any matching custom action names. + /// + /// List of names to search for in the environment + /// variable string. + /// True if a match was found, else false. + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal static bool DebugBreakEnabled(string[] names) + { + string mmsibreak = Environment.GetEnvironmentVariable("MMsiBreak"); + if (mmsibreak != null) + { + foreach (string breakName in mmsibreak.Split(',', ';')) + { + foreach (string name in names) + { + if (breakName == name) + { + return true; + } + } + } + } + return false; + } + + /// + /// Locates and parses an entrypoint mapping in CustomAction.config. + /// + /// Installer session handle, just used for logging. + /// Custom action entrypoint name: the key value + /// in an item in the customActions section of the config file. + /// Returned display name of the assembly from + /// the entrypoint mapping. + /// Returned class name of the entrypoint mapping. + /// Returned method name of the entrypoint mapping. + /// True if the entrypoint was found, false if not or if some error + /// occurred. + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + private static bool FindEntryPoint( + Session session, + string entryPoint, + out string assemblyName, + out string className, + out string methodName) + { + assemblyName = null; + className = null; + methodName = null; + + string fullEntryPoint; + if (entryPoint.IndexOf('!') > 0) + { + fullEntryPoint = entryPoint; + } + else + { + IDictionary config; + try + { + config = (IDictionary) ConfigurationManager.GetSection("customActions"); + } + catch (ConfigurationException cex) + { + session.Log("Error: missing or invalid customActions config section."); + session.Log(cex.ToString()); + return false; + } + fullEntryPoint = (string) config[entryPoint]; + if (fullEntryPoint == null) + { + session.Log( + "Error: custom action entry point '{0}' not found " + + "in customActions config section.", + entryPoint); + return false; + } + } + + int assemblySplit = fullEntryPoint.IndexOf('!'); + int methodSplit = fullEntryPoint.LastIndexOf('.'); + if (assemblySplit < 0 || methodSplit < 0 || methodSplit < assemblySplit) + { + session.Log("Error: invalid custom action entry point:" + entryPoint); + return false; + } + + assemblyName = fullEntryPoint.Substring(0, assemblySplit); + className = fullEntryPoint.Substring(assemblySplit + 1, methodSplit - assemblySplit - 1); + methodName = fullEntryPoint.Substring(methodSplit + 1); + return true; + } + + /// + /// Uses reflection to load the assembly and class and find the method. + /// + /// Installer session handle, just used for logging. + /// Display name of the assembly containing the + /// custom action method. + /// Fully-qualified name of the class containing the + /// custom action method. + /// Name of the custom action method. + /// The method, or null if not found. + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + private static MethodInfo GetCustomActionMethod( + Session session, + string assemblyName, + string className, + string methodName) + { + Assembly customActionAssembly; + Type customActionClass = null; + Exception caughtEx = null; + try + { + customActionAssembly = AppDomain.CurrentDomain.Load(assemblyName); + customActionClass = customActionAssembly.GetType(className, true, true); + } + catch (IOException ex) { caughtEx = ex; } + catch (BadImageFormatException ex) { caughtEx = ex; } + catch (TypeLoadException ex) { caughtEx = ex; } + catch (ReflectionTypeLoadException ex) { caughtEx = ex; } + catch (SecurityException ex) { caughtEx = ex; } + if (caughtEx != null) + { + session.Log("Error: could not load custom action class " + className + " from assembly: " + assemblyName); + session.Log(caughtEx.ToString()); + return null; + } + + MethodInfo[] methods = customActionClass.GetMethods( + BindingFlags.Public | BindingFlags.Static); + foreach (MethodInfo method in methods) + { + if (method.Name == methodName && + CustomActionProxy.MethodHasCustomActionSignature(method)) + { + return method; + } + } + session.Log("Error: custom action method \"" + methodName + + "\" is missing or has the wrong signature."); + return null; + } + + /// + /// Checks if a method has the right return and paramater types + /// for a custom action, and that it is marked by a CustomActionAttribute. + /// + /// Method to be checked. + /// True if the method is a valid custom action, else false. + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + private static bool MethodHasCustomActionSignature(MethodInfo method) + { + if (method.ReturnType == typeof(ActionResult) && + method.GetParameters().Length == 1 && + method.GetParameters()[0].ParameterType == typeof(Session)) + { + object[] methodAttribs = method.GetCustomAttributes(false); + foreach (object attrib in methodAttribs) + { + if (attrib is CustomActionAttribute) + { + return true; + } + } + } + return false; + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Database.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Database.cs new file mode 100644 index 000000000..d6c34e2e1 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Database.cs @@ -0,0 +1,942 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + + /// + /// Accesses a Windows Installer database. + /// + ///

+ /// The method must be called before the Database is closed to write out all + /// persistent changes. If the Commit method is not called, the installer performs an implicit + /// rollback upon object destruction. + ///

+ /// The client can use the following procedure for data access: + /// Obtain a Database object using one of the Database constructors. + /// Initiate a query using a SQL string by calling the + /// method of the Database. + /// Set query parameters in a and execute the database + /// query by calling the method of the . This + /// produces a result that can be fetched or updated. + /// Call the method of the View repeatedly to return + /// Records. + /// Update database rows of a Record object obtained by the Fetch method using + /// one of the methods of the View. + /// Release the query and any unfetched records by calling the + /// method of the View. + /// Persist any database updates by calling the Commit method of the Database. + /// + /// + ///

+ internal partial class Database : InstallerHandle + { + private string filePath; + private DatabaseOpenMode openMode; + private SummaryInfo summaryInfo; + private TableCollection tables; + private IList deleteOnClose; + + /// + /// Opens an existing database in read-only mode. + /// + /// Path to the database file. + /// the database could not be created/opened + ///

+ /// Because this constructor initiates database access, it cannot be used with a + /// running installation. + ///

+ /// The Database object should be d after use. + /// It is best that the handle be closed manually as soon as it is no longer + /// needed, as leaving lots of unused handles open can degrade performance. + ///

+ /// Win32 MSI API: + /// MsiOpenDatabase + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public Database(string filePath) + : this(filePath, DatabaseOpenMode.ReadOnly) + { + } + + /// + /// Opens an existing database with another database as output. + /// + /// Path to the database to be read. + /// Open mode for the database + /// Database object representing the created or opened database + /// the database could not be created/opened + ///

+ /// When a database is opened as the output of another database, the summary information stream + /// of the output database is actually a read-only mirror of the original database and thus cannot + /// be changed. Additionally, it is not persisted with the database. To create or modify the + /// summary information for the output database it must be closed and re-opened. + ///

+ /// The Database object should be d after use. + /// It is best that the handle be closed manually as soon as it is no longer + /// needed, as leaving lots of unused handles open can degrade performance. + ///

+ /// The database is opened in mode, and will be + /// automatically commited when it is closed. + ///

+ /// Win32 MSI API: + /// MsiOpenDatabase + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public Database(string filePath, string outputPath) + : this((IntPtr) Database.Open(filePath, outputPath), true, outputPath, DatabaseOpenMode.CreateDirect) + { + } + + /// + /// Opens an existing database or creates a new one. + /// + /// Path to the database file. If an empty string + /// is supplied, a temporary database is created that is not persisted. + /// Open mode for the database + /// the database could not be created/opened + ///

+ /// Because this constructor initiates database access, it cannot be used with a + /// running installation. + ///

+ /// The database object should be d after use. + /// The finalizer will close the handle if it is still open, however due to the nondeterministic + /// nature of finalization it is best that the handle be closed manually as soon as it is no + /// longer needed, as leaving lots of unused handles open can degrade performance. + ///

+ /// A database opened in or + /// mode will be automatically commited when it is + /// closed. However a database opened in or + /// mode must have the method + /// called before it is closed, otherwise no changes will be persisted. + ///

+ /// Win32 MSI API: + /// MsiOpenDatabase + ///

+ public Database(string filePath, DatabaseOpenMode mode) + : this((IntPtr) Database.Open(filePath, mode), true, filePath, mode) + { + } + + /// + /// Creates a new database from an MSI handle. + /// + /// Native MSI database handle. + /// True if the handle should be closed + /// when the database object is disposed + /// Path of the database file, if known + /// Mode the handle was originally opened in + protected internal Database( + IntPtr handle, bool ownsHandle, string filePath, DatabaseOpenMode openMode) + : base(handle, ownsHandle) + { + this.filePath = filePath; + this.openMode = openMode; + } + + /// + /// Gets the file path the Database was originally opened from, or null if not known. + /// + public String FilePath + { + get + { + return this.filePath; + } + } + + /// + /// Gets the open mode for the database. + /// + public DatabaseOpenMode OpenMode + { + get + { + return this.openMode; + } + } + + /// + /// Gets a boolean value indicating whether this database was opened in read-only mode. + /// + ///

+ /// Win32 MSI API: + /// MsiGetDatabaseState + ///

+ public bool IsReadOnly + { + get + { + if (RemotableNativeMethods.RemotingEnabled) + { + return true; + } + + int state = NativeMethods.MsiGetDatabaseState((int) this.Handle); + return state != 1; + } + } + + /// + /// Gets the collection of tables in the Database. + /// + public TableCollection Tables + { + get + { + if (this.tables == null) + { + this.tables = new TableCollection(this); + } + return this.tables; + } + } + + /// + /// Gets or sets the code page of the Database. + /// + /// error exporting/importing the codepage data + /// the Database handle is invalid + ///

+ /// Getting or setting the code page is a slow operation because it involves an export or import + /// of the codepage data to/from a temporary file. + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public int CodePage + { + get + { + string tempFile = Path.GetTempFileName(); + StreamReader reader = null; + try + { + this.Export("_ForceCodepage", tempFile); + reader = File.OpenText(tempFile); + reader.ReadLine(); // Skip column name record. + reader.ReadLine(); // Skip column defn record. + string codePageLine = reader.ReadLine(); + return Int32.Parse(codePageLine.Split('\t')[0], CultureInfo.InvariantCulture.NumberFormat); + } + finally + { + if (reader != null) reader.Close(); + File.Delete(tempFile); + } + } + + set + { + string tempFile = Path.GetTempFileName(); + StreamWriter writer = null; + try + { + writer = File.AppendText(tempFile); + writer.WriteLine(""); + writer.WriteLine(""); + writer.WriteLine("{0}\t_ForceCodepage", value); + writer.Close(); + writer = null; + this.Import(tempFile); + } + finally + { + if (writer != null) writer.Close(); + File.Delete(tempFile); + } + } + } + + /// + /// Gets the SummaryInfo object for this database that can be used to examine and modify properties + /// to the summary information stream. + /// + /// the Database handle is invalid + ///

+ /// The object returned from this property does not need to be explicitly persisted or closed. + /// Any modifications will be automatically saved when the database is committed. + ///

+ /// Win32 MSI API: + /// MsiGetSummaryInformation + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public SummaryInfo SummaryInfo + { + get + { + if (this.summaryInfo == null || this.summaryInfo.IsClosed) + { + lock (this.Sync) + { + if (this.summaryInfo == null || this.summaryInfo.IsClosed) + { + int summaryInfoHandle; + int maxProperties = this.IsReadOnly ? 0 : SummaryInfo.MAX_PROPERTIES; + uint ret = RemotableNativeMethods.MsiGetSummaryInformation((int) this.Handle, null, (uint) maxProperties, out summaryInfoHandle); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + this.summaryInfo = new SummaryInfo((IntPtr) summaryInfoHandle, true); + } + } + } + return this.summaryInfo; + } + } + + /// + /// Creates a new Database object from an integer database handle. + /// + ///

+ /// This method is only provided for interop purposes. A Database object + /// should normally be obtained from or + /// a public Database constructor. + ///

+ /// Integer database handle + /// true to close the handle when this object is disposed + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static Database FromHandle(IntPtr handle, bool ownsHandle) + { + return new Database( + handle, + ownsHandle, + null, + NativeMethods.MsiGetDatabaseState((int) handle) == 1 ? DatabaseOpenMode.Direct : DatabaseOpenMode.ReadOnly); + } + + /// + /// Schedules a file or directory for deletion after the database handle is closed. + /// + /// File or directory path to be deleted. All files and subdirectories + /// under a directory are deleted. + ///

+ /// Once an item is scheduled, it cannot be unscheduled. + ///

+ /// The items cannot be deleted if the Database object is auto-disposed by the + /// garbage collector; the handle must be explicitly closed. + ///

+ /// Files which are read-only or otherwise locked cannot be deleted, + /// but they will not cause an exception to be thrown. + ///

+ public void DeleteOnClose(string path) + { + if (this.deleteOnClose == null) + { + this.deleteOnClose = new List(); + } + this.deleteOnClose.Add(path); + } + + /// + /// Merges another database with this database. + /// + /// The database to be merged into this database + /// Optional name of table to contain the names of the tables containing + /// merge conflicts, the number of conflicting rows within the table, and a reference to the table + /// with the merge conflict. + /// merge failed due to a schema difference or data conflict + /// the Database handle is invalid + ///

+ /// Merge does not copy over embedded cabinet files or embedded transforms from the + /// reference database into the target database. Embedded data streams that are listed in the + /// Binary table or Icon table are copied from the reference database to the target database. + /// Storage embedded in the reference database are not copied to the target database. + ///

+ /// The Merge method merges the data of two databases. These databases must have the same + /// codepage. The merge fails if any tables or rows in the databases conflict. A conflict exists + /// if the data in any row in the first database differs from the data in the corresponding row + /// of the second database. Corresponding rows are in the same table of both databases and have + /// the same primary key in both databases. The tables of non-conflicting databases must have + /// the same number of primary keys, same number of columns, same column types, same column names, + /// and the same data in rows with identical primary keys. Temporary columns however don't matter + /// in the column count and corresponding tables can have a different number of temporary columns + /// without creating conflict as long as the persistent columns match. + ///

+ /// If the number, type, or name of columns in corresponding tables are different, the + /// schema of the two databases are incompatible and the installer will stop processing tables + /// and the merge fails. The installer checks that the two databases have the same schema before + /// checking for row merge conflicts. If the schemas are incompatible, the databases have be + /// modified. + ///

+ /// If the data in particular rows differ, this is a row merge conflict, the merge fails + /// and creates a new table with the specified name. The first column of this table is the name + /// of the table having the conflict. The second column gives the number of rows in the table + /// having the conflict. + ///

+ /// Win32 MSI API: + /// MsiDatabaseMerge + ///

+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void Merge(Database otherDatabase, string errorTable) + { + if (otherDatabase == null) + { + throw new ArgumentNullException("otherDatabase"); + } + + uint ret = NativeMethods.MsiDatabaseMerge((int) this.Handle, (int) otherDatabase.Handle, errorTable); + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.FUNCTION_FAILED) + { + throw new MergeException(this, errorTable); + } + else if (ret == (uint) NativeMethods.Error.DATATYPE_MISMATCH) + { + throw new MergeException("Schema difference between the two databases."); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + } + + /// + /// Merges another database with this database. + /// + /// The database to be merged into this database + /// merge failed due to a schema difference or data conflict + /// the Database handle is invalid + ///

+ /// MsiDatabaseMerge does not copy over embedded cabinet files or embedded transforms from + /// the reference database into the target database. Embedded data streams that are listed in + /// the Binary table or Icon table are copied from the reference database to the target database. + /// Storage embedded in the reference database are not copied to the target database. + ///

+ /// The Merge method merges the data of two databases. These databases must have the same + /// codepage. The merge fails if any tables or rows in the databases conflict. A conflict exists + /// if the data in any row in the first database differs from the data in the corresponding row + /// of the second database. Corresponding rows are in the same table of both databases and have + /// the same primary key in both databases. The tables of non-conflicting databases must have + /// the same number of primary keys, same number of columns, same column types, same column names, + /// and the same data in rows with identical primary keys. Temporary columns however don't matter + /// in the column count and corresponding tables can have a different number of temporary columns + /// without creating conflict as long as the persistent columns match. + ///

+ /// If the number, type, or name of columns in corresponding tables are different, the + /// schema of the two databases are incompatible and the installer will stop processing tables + /// and the merge fails. The installer checks that the two databases have the same schema before + /// checking for row merge conflicts. If the schemas are incompatible, the databases have be + /// modified. + ///

+ /// Win32 MSI API: + /// MsiDatabaseMerge + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void Merge(Database otherDatabase) { this.Merge(otherDatabase, null); } + + /// + /// Checks whether a table exists and is persistent in the database. + /// + /// The table to the checked + /// true if the table exists and is persistent in the database; false otherwise + /// the table is unknown + /// the Database handle is invalid + ///

+ /// To check whether a table exists regardless of persistence, + /// use . + ///

+ /// Win32 MSI API: + /// MsiDatabaseIsTablePersistent + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool IsTablePersistent(string table) + { + if (string.IsNullOrWhiteSpace(table)) + { + throw new ArgumentNullException("table"); + } + uint ret = RemotableNativeMethods.MsiDatabaseIsTablePersistent((int) this.Handle, table); + if (ret == 3) // MSICONDITION_ERROR + { + throw new InstallerException(); + } + return ret == 1; + } + + /// + /// Checks whether a table contains a persistent column with a given name. + /// + /// The table to the checked + /// The name of the column to be checked + /// true if the column exists in the table; false if the column is temporary or does not exist. + /// the View could not be executed + /// the Database handle is invalid + ///

+ /// To check whether a column exists regardless of persistence, + /// use . + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool IsColumnPersistent(string table, string column) + { + if (string.IsNullOrWhiteSpace(table)) + { + throw new ArgumentNullException("table"); + } + if (string.IsNullOrWhiteSpace(column)) + { + throw new ArgumentNullException("column"); + } + using (View view = this.OpenView( + "SELECT `Number` FROM `_Columns` WHERE `Table` = '{0}' AND `Name` = '{1}'", table, column)) + { + view.Execute(); + using (Record rec = view.Fetch()) + { + return (rec != null); + } + } + } + + /// + /// Gets the count of all rows in the table. + /// + /// Name of the table whose rows are to be counted + /// The count of all rows in the table + /// the View could not be executed + /// the Database handle is invalid + public int CountRows(string table) + { + return this.CountRows(table, null); + } + + /// + /// Gets the count of all rows in the table that satisfy a given condition. + /// + /// Name of the table whose rows are to be counted + /// Conditional expression, such as could be placed on the end of a SQL WHERE clause + /// The count of all rows in the table satisfying the condition + /// the SQL WHERE syntax is invalid + /// the View could not be executed + /// the Database handle is invalid + public int CountRows(string table, string where) + { + if (string.IsNullOrWhiteSpace(table)) + { + throw new ArgumentNullException("table"); + } + + int count; + using (View view = this.OpenView( + "SELECT `{0}` FROM `{1}`{2}", + this.Tables[table].PrimaryKeys[0], + table, + (where != null && where.Length != 0 ? " WHERE " + where : ""))) + { + view.Execute(); + for (count = 0; ; count++) + { + // Avoid creating unnecessary Record objects by not calling View.Fetch(). + int recordHandle; + uint ret = RemotableNativeMethods.MsiViewFetch((int) view.Handle, out recordHandle); + if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) + { + break; + } + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + RemotableNativeMethods.MsiCloseHandle(recordHandle); + } + } + return count; + } + + /// + /// Finalizes the persistent form of the database. All persistent data is written + /// to the writeable database, and no temporary columns or rows are written. + /// + /// the Database handle is invalid + ///

+ /// For a database open in mode, this method has no effect. + ///

+ /// For a database open in or + /// mode, it is not necessary to call this method because the database will be automatically committed + /// when it is closed. However this method may be called at any time to persist the current state of tables + /// loaded into memory. + ///

+ /// For a database open in or + /// mode, no changes will be persisted until this method is called. If the database object is closed without + /// calling this method, the database file remains unmodified. + ///

+ /// Win32 MSI API: + /// MsiDatabaseCommit + ///

+ public void Commit() + { + if (this.summaryInfo != null && !this.summaryInfo.IsClosed) + { + this.summaryInfo.Persist(); + this.summaryInfo.Close(); + this.summaryInfo = null; + } + uint ret = NativeMethods.MsiDatabaseCommit((int) this.Handle); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Copies the structure and data from a specified table to a text archive file. + /// + /// Name of the table to be exported + /// Path to the file to be created + /// the file path is invalid + /// the Database handle is invalid + ///

+ /// Win32 MSI API: + /// MsiDatabaseExport + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void Export(string table, string exportFilePath) + { + if (table == null) + { + throw new ArgumentNullException("table"); + } + + FileInfo file = new FileInfo(exportFilePath); + uint ret = NativeMethods.MsiDatabaseExport((int) this.Handle, table, file.DirectoryName, file.Name); + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.BAD_PATHNAME) + { + throw new FileNotFoundException(null, exportFilePath); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + } + + /// + /// Imports a database table from a text archive file, dropping any existing table. + /// + /// Path to the file to be imported. + /// The table name is specified within the file. + /// the file path is invalid + /// the Database handle is invalid + ///

+ /// Win32 MSI API: + /// MsiDatabaseImport + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void Import(string importFilePath) + { + if (string.IsNullOrWhiteSpace(importFilePath)) + { + throw new ArgumentNullException("importFilePath"); + } + + FileInfo file = new FileInfo(importFilePath); + uint ret = NativeMethods.MsiDatabaseImport((int) this.Handle, file.DirectoryName, file.Name); + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.BAD_PATHNAME) + { + throw new FileNotFoundException(null, importFilePath); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + } + + /// + /// Exports all database tables, streams, and summary information to archive files. + /// + /// Path to the directory where archive files will be created + /// the directory path is invalid + /// the Database handle is invalid + ///

+ /// The directory will be created if it does not already exist. + ///

+ /// Win32 MSI API: + /// MsiDatabaseExport + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void ExportAll(string directoryPath) + { + if (string.IsNullOrWhiteSpace(directoryPath)) + { + throw new ArgumentNullException("directoryPath"); + } + + if (!Directory.Exists(directoryPath)) + { + Directory.CreateDirectory(directoryPath); + } + + this.Export("_SummaryInformation", Path.Combine(directoryPath, "_SummaryInformation.idt")); + + using (View view = this.OpenView("SELECT `Name` FROM `_Tables`")) + { + view.Execute(); + + foreach (Record rec in view) using (rec) + { + string table = (string) rec[1]; + + this.Export(table, Path.Combine(directoryPath, table + ".idt")); + } + } + + if (!Directory.Exists(Path.Combine(directoryPath, "_Streams"))) + { + Directory.CreateDirectory(Path.Combine(directoryPath, "_Streams")); + } + + using (View view = this.OpenView("SELECT `Name`, `Data` FROM `_Streams`")) + { + view.Execute(); + + foreach (Record rec in view) using (rec) + { + string stream = (string) rec[1]; + if (stream.EndsWith("SummaryInformation", StringComparison.Ordinal)) continue; + + int i = stream.IndexOf('.'); + if (i >= 0) + { + if (File.Exists(Path.Combine( + directoryPath, + Path.Combine(stream.Substring(0, i), stream.Substring(i + 1) + ".ibd")))) + { + continue; + } + } + rec.GetStream(2, Path.Combine(directoryPath, Path.Combine("_Streams", stream))); + } + } + } + + /// + /// Imports all database tables, streams, and summary information from archive files. + /// + /// Path to the directory from which archive files will be imported + /// the directory path is invalid + /// the Database handle is invalid + ///

+ /// Win32 MSI API: + /// MsiDatabaseImport + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void ImportAll(string directoryPath) + { + if (string.IsNullOrWhiteSpace(directoryPath)) + { + throw new ArgumentNullException("directoryPath"); + } + + if (File.Exists(Path.Combine(directoryPath, "_SummaryInformation.idt"))) + { + this.Import(Path.Combine(directoryPath, "_SummaryInformation.idt")); + } + + string[] idtFiles = Directory.GetFiles(directoryPath, "*.idt"); + foreach (string file in idtFiles) + { + if (Path.GetFileName(file) != "_SummaryInformation.idt") + { + this.Import(file); + } + } + + if (Directory.Exists(Path.Combine(directoryPath, "_Streams"))) + { + View view = this.OpenView("SELECT `Name`, `Data` FROM `_Streams`"); + Record rec = null; + try + { + view.Execute(); + string[] streamFiles = Directory.GetFiles(Path.Combine(directoryPath, "_Streams")); + foreach (string file in streamFiles) + { + rec = this.CreateRecord(2); + rec[1] = Path.GetFileName(file); + rec.SetStream(2, file); + view.Insert(rec); + rec.Close(); + rec = null; + } + } + finally + { + if (rec != null) rec.Close(); + view.Close(); + } + } + } + + /// + /// Creates a new record object with the requested number of fields. + /// + /// Required number of fields, which may be 0. + /// The maximum number of fields in a record is limited to 65535. + /// A new record object that can be used with the database. + ///

+ /// This method is equivalent to directly calling the + /// constructor in all cases outside of a custom action context. When in a + /// custom action session, this method allows creation of a record that can + /// work with a database other than the session database. + ///

+ /// The Record object should be d after use. + /// It is best that the handle be closed manually as soon as it is no longer + /// needed, as leaving lots of unused handles open can degrade performance. + ///

+ /// Win32 MSI API: + /// MsiCreateRecord + ///

+ public Record CreateRecord(int fieldCount) + { + int hRecord = RemotableNativeMethods.MsiCreateRecord((uint) fieldCount, (int) this.Handle); + return new Record((IntPtr) hRecord, true, (View) null); + } + + /// + /// Returns the file path of this database, or the handle value if a file path was not specified. + /// + public override string ToString() + { + if (this.FilePath != null) + { + return this.FilePath; + } + else + { + return "#" + ((int) this.Handle).ToString(CultureInfo.InvariantCulture); + } + } + + /// + /// Closes the database handle. After closing a handle, further method calls may throw . + /// + /// If true, the method has been called directly or + /// indirectly by a user's code, so managed and unmanaged resources will be + /// disposed. If false, only unmanaged resources will be disposed. + protected override void Dispose(bool disposing) + { + if (!this.IsClosed && + (this.OpenMode == DatabaseOpenMode.CreateDirect || + this.OpenMode == DatabaseOpenMode.Direct)) + { + // Always commit a direct-opened database before closing. + // This avoids unexpected corruption of the database. + this.Commit(); + } + + base.Dispose(disposing); + + if (disposing) + { + if (this.summaryInfo != null) + { + this.summaryInfo.Close(); + this.summaryInfo = null; + } + + if (this.deleteOnClose != null) + { + foreach (string path in this.deleteOnClose) + { + try + { + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + else + { + if (File.Exists(path)) File.Delete(path); + } + } + catch (IOException) + { + } + catch (UnauthorizedAccessException) + { + } + } + this.deleteOnClose = null; + } + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + private static int Open(string filePath, string outputPath) + { + if (string.IsNullOrWhiteSpace(filePath)) + { + throw new ArgumentNullException("filePath"); + } + + if (string.IsNullOrWhiteSpace(outputPath)) + { + throw new ArgumentNullException("outputPath"); + } + + int hDb; + uint ret = NativeMethods.MsiOpenDatabase(filePath, outputPath, out hDb); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return hDb; + } + + private static int Open(string filePath, DatabaseOpenMode mode) + { + if (string.IsNullOrWhiteSpace(filePath)) + { + throw new ArgumentNullException("filePath"); + } + + if (Path.GetExtension(filePath).Equals(".msp", StringComparison.Ordinal)) + { + const int DATABASEOPENMODE_PATCH = 32; + int patchMode = (int) mode | DATABASEOPENMODE_PATCH; + mode = (DatabaseOpenMode) patchMode; + } + + int hDb; + uint ret = NativeMethods.MsiOpenDatabase(filePath, (IntPtr) mode, out hDb); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode( + ret, + String.Format(CultureInfo.InvariantCulture, "Database=\"{0}\"", filePath)); + } + return hDb; + } + + /// + /// Returns the value of the specified property. + /// + /// Name of the property to retrieve. + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string ExecutePropertyQuery(string property) + { + IList values = this.ExecuteStringQuery("SELECT `Value` FROM `Property` WHERE `Property` = '{0}'", property); + return (values.Count > 0 ? values[0] : null); + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/DatabaseQuery.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/DatabaseQuery.cs new file mode 100644 index 000000000..0127ace8c --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/DatabaseQuery.cs @@ -0,0 +1,424 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Globalization; + + internal partial class Database + { + /// + /// Gets a View object representing the query specified by a SQL string. + /// + /// SQL query string, which may contain format items + /// Zero or more objects to format + /// A View object representing the query specified by a SQL string + /// the SQL syntax is invalid + /// the Database handle is invalid + ///

+ /// The parameter is formatted using . + ///

+ /// The View object should be d after use. + /// It is best that the handle be closed manually as soon as it is no longer + /// needed, as leaving lots of unused handles open can degrade performance. + ///

+ /// Win32 MSI API: + /// MsiDatabaseOpenView + ///

+ public View OpenView(string sqlFormat, params object[] args) + { + if (string.IsNullOrWhiteSpace(sqlFormat)) + { + throw new ArgumentNullException("sqlFormat"); + } + + string sql = (args == null || args.Length == 0 ? sqlFormat : + String.Format(CultureInfo.InvariantCulture, sqlFormat, args)); + int viewHandle; + uint ret = RemotableNativeMethods.MsiDatabaseOpenView((int) this.Handle, sql, out viewHandle); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + return new View((IntPtr) viewHandle, sql, this); + } + + /// + /// Executes the query specified by a SQL string. The query may not be a SELECT statement. + /// + /// SQL query string, which may contain format items + /// Zero or more objects to format + /// the SQL syntax is invalid + /// the View could not be executed + /// the Database handle is invalid + ///

+ /// The parameter is formatted using + /// . + ///

+ /// Win32 MSI APIs: + /// MsiDatabaseOpenView, + /// MsiViewExecute + ///

+ public void Execute(string sqlFormat, params object[] args) + { + if (string.IsNullOrWhiteSpace(sqlFormat)) + { + throw new ArgumentNullException("sqlFormat"); + } + + this.Execute( + args == null || args.Length == 0 ? + sqlFormat : String.Format(CultureInfo.InvariantCulture, sqlFormat, args), + (Record) null); + } + + /// + /// Executes the query specified by a SQL string. The query may not be a SELECT statement. + /// + /// SQL query string + /// Optional Record object containing the values that replace + /// the parameter tokens (?) in the SQL query. + /// the SQL syntax is invalid + /// the View could not be executed + /// the Database handle is invalid + ///

+ /// Win32 MSI APIs: + /// MsiDatabaseOpenView, + /// MsiViewExecute + ///

+ public void Execute(string sql, Record record) + { + if (string.IsNullOrWhiteSpace(sql)) + { + throw new ArgumentNullException("sql"); + } + + using (View view = this.OpenView(sql)) + { + view.Execute(record); + } + } + + /// + /// Executes the specified SQL SELECT query and returns all results. + /// + /// SQL query string, which may contain format items + /// Zero or more objects to format + /// All results combined into an array + /// the SQL syntax is invalid + /// the View could not be executed + /// the Database handle is invalid + ///

+ /// The parameter is formatted using + /// . + ///

+ /// Multiple rows columns will be collapsed into a single one-dimensional list. + ///

+ /// Win32 MSI APIs: + /// MsiDatabaseOpenView, + /// MsiViewExecute, + /// MsiViewFetch + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public IList ExecuteQuery(string sqlFormat, params object[] args) + { + if (string.IsNullOrWhiteSpace(sqlFormat)) + { + throw new ArgumentNullException("sqlFormat"); + } + + return this.ExecuteQuery( + args == null || args.Length == 0 ? + sqlFormat : String.Format(CultureInfo.InvariantCulture, sqlFormat, args), + (Record) null); + } + + /// + /// Executes the specified SQL SELECT query and returns all results. + /// + /// SQL SELECT query string + /// Optional Record object containing the values that replace + /// the parameter tokens (?) in the SQL query. + /// All results combined into an array + /// the SQL syntax is invalid + /// the View could not be executed + /// the Database handle is invalid + ///

+ /// Multiple rows columns will be collapsed into a single one-dimensional list. + ///

+ /// Win32 MSI APIs: + /// MsiDatabaseOpenView, + /// MsiViewExecute, + /// MsiViewFetch + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public IList ExecuteQuery(string sql, Record record) + { + if (string.IsNullOrWhiteSpace(sql)) + { + throw new ArgumentNullException("sql"); + } + + using (View view = this.OpenView(sql)) + { + view.Execute(record); + IList results = new ArrayList(); + int fieldCount = 0; + + foreach (Record rec in view) using (rec) + { + if (fieldCount == 0) fieldCount = rec.FieldCount; + for (int i = 1; i <= fieldCount; i++) + { + results.Add(rec[i]); + } + } + + return results; + } + } + + /// + /// Executes the specified SQL SELECT query and returns all results as integers. + /// + /// SQL query string, which may contain format items + /// Zero or more objects to format + /// All results combined into an array + /// the SQL syntax is invalid + /// the View could not be executed + /// the Database handle is invalid + ///

+ /// The parameter is formatted using + /// . + ///

+ /// Multiple rows columns will be collapsed into a single one-dimensional list. + ///

+ /// Win32 MSI APIs: + /// MsiDatabaseOpenView, + /// MsiViewExecute, + /// MsiViewFetch + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public IList ExecuteIntegerQuery(string sqlFormat, params object[] args) + { + if (string.IsNullOrWhiteSpace(sqlFormat)) + { + throw new ArgumentNullException("sqlFormat"); + } + + return this.ExecuteIntegerQuery( + args == null || args.Length == 0 ? + sqlFormat : String.Format(CultureInfo.InvariantCulture, sqlFormat, args), + (Record) null); + } + + /// + /// Executes the specified SQL SELECT query and returns all results as integers. + /// + /// SQL SELECT query string + /// Optional Record object containing the values that replace + /// the parameter tokens (?) in the SQL query. + /// All results combined into an array + /// the SQL syntax is invalid + /// the View could not be executed + /// the Database handle is invalid + ///

+ /// Multiple rows columns will be collapsed into a single one-dimensional list. + ///

+ /// Win32 MSI APIs: + /// MsiDatabaseOpenView, + /// MsiViewExecute, + /// MsiViewFetch + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public IList ExecuteIntegerQuery(string sql, Record record) + { + if (string.IsNullOrWhiteSpace(sql)) + { + throw new ArgumentNullException("sql"); + } + + using (View view = this.OpenView(sql)) + { + view.Execute(record); + IList results = new List(); + int fieldCount = 0; + + foreach (Record rec in view) using (rec) + { + if (fieldCount == 0) fieldCount = rec.FieldCount; + for (int i = 1; i <= fieldCount; i++) + { + results.Add(rec.GetInteger(i)); + } + } + + return results; + } + } + + /// + /// Executes the specified SQL SELECT query and returns all results as strings. + /// + /// SQL query string, which may contain format items + /// Zero or more objects to format + /// All results combined into an array + /// the SQL syntax is invalid + /// the View could not be executed + /// the Database handle is invalid + ///

+ /// The parameter is formatted using + /// . + ///

+ /// Multiple rows columns will be collapsed into a single on-dimensional list. + ///

+ /// Win32 MSI APIs: + /// MsiDatabaseOpenView, + /// MsiViewExecute, + /// MsiViewFetch + ///

+ public IList ExecuteStringQuery(string sqlFormat, params object[] args) + { + if (string.IsNullOrWhiteSpace(sqlFormat)) + { + throw new ArgumentNullException("sqlFormat"); + } + + return this.ExecuteStringQuery( + args == null || args.Length == 0 ? + sqlFormat : String.Format(CultureInfo.InvariantCulture, sqlFormat, args), + (Record) null); + } + + /// + /// Executes the specified SQL SELECT query and returns all results as strings. + /// + /// SQL SELECT query string + /// Optional Record object containing the values that replace + /// the parameter tokens (?) in the SQL query. + /// All results combined into an array + /// the SQL syntax is invalid + /// the View could not be executed + /// the Database handle is invalid + ///

+ /// Multiple rows columns will be collapsed into a single on-dimensional list. + ///

+ /// Win32 MSI APIs: + /// MsiDatabaseOpenView, + /// MsiViewExecute, + /// MsiViewFetch + ///

+ public IList ExecuteStringQuery(string sql, Record record) + { + if (string.IsNullOrWhiteSpace(sql)) + { + throw new ArgumentNullException("sql"); + } + + using (View view = this.OpenView(sql)) + { + view.Execute(record); + IList results = new List(); + int fieldCount = 0; + + foreach (Record rec in view) using (rec) + { + if (fieldCount == 0) fieldCount = rec.FieldCount; + for (int i = 1; i <= fieldCount; i++) + { + results.Add(rec.GetString(i)); + } + } + + return results; + } + } + + /// + /// Executes the specified SQL SELECT query and returns a single result. + /// + /// SQL query string, which may contain format items + /// Zero or more objects to format + /// First field of the first result + /// the SQL syntax is invalid + /// the View could not be executed + /// or the query returned 0 results + /// the Database handle is invalid + ///

+ /// The parameter is formatted using + /// . + ///

+ /// Win32 MSI APIs: + /// MsiDatabaseOpenView, + /// MsiViewExecute, + /// MsiViewFetch + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public object ExecuteScalar(string sqlFormat, params object[] args) + { + if (string.IsNullOrWhiteSpace(sqlFormat)) + { + throw new ArgumentNullException("sqlFormat"); + } + + return this.ExecuteScalar( + args == null || args.Length == 0 ? + sqlFormat : String.Format(CultureInfo.InvariantCulture, sqlFormat, args), + (Record) null); + } + + /// + /// Executes the specified SQL SELECT query and returns a single result. + /// + /// SQL SELECT query string + /// Optional Record object containing the values that replace + /// the parameter tokens (?) in the SQL query. + /// First field of the first result + /// the SQL syntax is invalid + /// the View could not be executed + /// or the query returned 0 results + /// the Database handle is invalid + ///

+ /// Win32 MSI APIs: + /// MsiDatabaseOpenView, + /// MsiViewExecute, + /// MsiViewFetch + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public object ExecuteScalar(string sql, Record record) + { + if (string.IsNullOrWhiteSpace(sql)) + { + throw new ArgumentNullException("sql"); + } + + View view = this.OpenView(sql); + Record rec = null; + try + { + view.Execute(record); + rec = view.Fetch(); + if (rec == null) + { + throw InstallerException.ExceptionFromReturnCode((uint) NativeMethods.Error.NO_MORE_ITEMS); + } + return rec[1]; + } + finally + { + if (rec != null) rec.Close(); + view.Close(); + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/DatabaseTransform.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/DatabaseTransform.cs new file mode 100644 index 000000000..6ceeea255 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/DatabaseTransform.cs @@ -0,0 +1,291 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + + internal partial class Database + { + /// + /// Creates a transform that, when applied to the object database, results in the reference database. + /// + /// Database that does not include the changes + /// Name of the generated transform file, or null to only + /// check whether or not the two database are identical + /// true if a transform is generated, or false if a transform is not generated + /// because there are no differences between the two databases. + /// the transform could not be generated + /// a Database handle is invalid + ///

+ /// A transform can add non-primary key columns to the end of a table. A transform cannot + /// be created that adds primary key columns to a table. A transform cannot be created that + /// changes the order, names, or definitions of columns. + ///

+ /// If the transform is to be applied during an installation you must use the + /// method to populate the + /// summary information stream. + ///

+ /// Win32 MSI API: + /// MsiDatabaseGenerateTransform + ///

+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool GenerateTransform(Database referenceDatabase, string transformFile) + { + if (referenceDatabase == null) + { + throw new ArgumentNullException("referenceDatabase"); + } + + if (string.IsNullOrWhiteSpace(transformFile)) + { + throw new ArgumentNullException("transformFile"); + } + + uint ret = NativeMethods.MsiDatabaseGenerateTransform((int) this.Handle, (int) referenceDatabase.Handle, transformFile, 0, 0); + if (ret == (uint) NativeMethods.Error.NO_DATA) + { + return false; + } + else if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return true; + } + + /// + /// Creates and populates the summary information stream of an existing transform file, and + /// fills in the properties with the base and reference ProductCode and ProductVersion. + /// + /// Database that does not include the changes + /// Name of the generated transform file + /// Error conditions that should be suppressed + /// when the transform is applied + /// Defines which properties should be validated + /// to verify that this transform can be applied to a database. + /// the transform summary info could not be + /// generated + /// a Database handle is invalid + ///

+ /// Win32 MSI API: + /// MsiCreateTransformSummaryInfo + ///

+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void CreateTransformSummaryInfo( + Database referenceDatabase, + string transformFile, + TransformErrors errors, + TransformValidations validations) + { + if (referenceDatabase == null) + { + throw new ArgumentNullException("referenceDatabase"); + } + + if (string.IsNullOrWhiteSpace(transformFile)) + { + throw new ArgumentNullException("transformFile"); + } + + uint ret = NativeMethods.MsiCreateTransformSummaryInfo( + (int) this.Handle, + (int) referenceDatabase.Handle, + transformFile, + (int) errors, + (int) validations); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Apply a transform to the database, recording the changes in the "_TransformView" table. + /// + /// Path to the transform file + /// the transform could not be applied + /// the Database handle is invalid + ///

+ /// Win32 MSI API: + /// MsiDatabaseApplyTransform + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void ViewTransform(string transformFile) + { + TransformErrors transformErrors = + TransformErrors.AddExistingRow | + TransformErrors.DelMissingRow | + TransformErrors.AddExistingTable | + TransformErrors.DelMissingTable | + TransformErrors.UpdateMissingRow | + TransformErrors.ChangeCodePage | + TransformErrors.ViewTransform; + this.ApplyTransform(transformFile, transformErrors); + } + + /// + /// Apply a transform to the database, suppressing any error conditions + /// specified by the transform's summary information. + /// + /// Path to the transform file + /// the transform could not be applied + /// the Database handle is invalid + ///

+ /// Win32 MSI API: + /// MsiDatabaseApplyTransform + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void ApplyTransform(string transformFile) + { + if (string.IsNullOrWhiteSpace(transformFile)) + { + throw new ArgumentNullException("transformFile"); + } + + TransformErrors errorConditionsToSuppress; + using (SummaryInfo transformSummInfo = new SummaryInfo(transformFile, false)) + { + int errorConditions = transformSummInfo.CharacterCount & 0xFFFF; + errorConditionsToSuppress = (TransformErrors) errorConditions; + } + this.ApplyTransform(transformFile, errorConditionsToSuppress); + } + + /// + /// Apply a transform to the database, specifying error conditions to suppress. + /// + /// Path to the transform file + /// Error conditions that are to be suppressed + /// the transform could not be applied + /// the Database handle is invalid + ///

+ /// Win32 MSI API: + /// MsiDatabaseApplyTransform + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void ApplyTransform(string transformFile, TransformErrors errorConditionsToSuppress) + { + if (string.IsNullOrWhiteSpace(transformFile)) + { + throw new ArgumentNullException("transformFile"); + } + + uint ret = NativeMethods.MsiDatabaseApplyTransform((int) this.Handle, transformFile, (int) errorConditionsToSuppress); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Checks whether a transform is valid for this Database, according to its validation data and flags. + /// + /// Path to the transform file + /// true if the transform can be validly applied to this Database; false otherwise + /// the transform could not be applied + /// the Database handle is invalid + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool IsTransformValid(string transformFile) + { + if (string.IsNullOrWhiteSpace(transformFile)) + { + throw new ArgumentNullException("transformFile"); + } + + using (SummaryInfo transformSummInfo = new SummaryInfo(transformFile, false)) + { + return this.IsTransformValid(transformSummInfo); + } + } + + /// + /// Checks whether a transform is valid for this Database, according to its SummaryInfo data. + /// + /// SummaryInfo data of a transform file + /// true if the transform can be validly applied to this Database; false otherwise + /// error processing summary info + /// the Database or SummaryInfo handle is invalid + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool IsTransformValid(SummaryInfo transformSummaryInfo) + { + if (transformSummaryInfo == null) + { + throw new ArgumentNullException("transformSummaryInfo"); + } + + string[] rev = transformSummaryInfo.RevisionNumber.Split(new char[] { ';' }, 3); + string targetProductCode = rev[0].Substring(0, 38); + string targetProductVersion = rev[0].Substring(38); + string upgradeCode = rev[2]; + + string[] templ = transformSummaryInfo.Template.Split(new char[] { ';' }, 2); + int targetProductLanguage = 0; + if (templ.Length >= 2 && templ[1].Length > 0) + { + targetProductLanguage = Int32.Parse(templ[1], CultureInfo.InvariantCulture.NumberFormat); + } + + int flags = transformSummaryInfo.CharacterCount; + int validateFlags = flags >> 16; + + string thisProductCode = this.ExecutePropertyQuery("ProductCode"); + string thisProductVersion = this.ExecutePropertyQuery("ProductVersion"); + string thisUpgradeCode = this.ExecutePropertyQuery("UpgradeCode"); + string thisProductLang = this.ExecutePropertyQuery("ProductLanguage"); + int thisProductLanguage = 0; + if (!string.IsNullOrWhiteSpace(thisProductLang)) + { + thisProductLanguage = Int32.Parse(thisProductLang, CultureInfo.InvariantCulture.NumberFormat); + } + + if ((validateFlags & (int) TransformValidations.Product) != 0 && + thisProductCode != targetProductCode) + { + return false; + } + + if ((validateFlags & (int) TransformValidations.UpgradeCode) != 0 && + thisUpgradeCode != upgradeCode) + { + return false; + } + + if ((validateFlags & (int) TransformValidations.Language) != 0 && + targetProductLanguage != 0 && thisProductLanguage != targetProductLanguage) + { + return false; + } + + Version thisProductVer = new Version(thisProductVersion); + Version targetProductVer = new Version(targetProductVersion); + if ((validateFlags & (int) TransformValidations.UpdateVersion) != 0) + { + if (thisProductVer.Major != targetProductVer.Major) return false; + if (thisProductVer.Minor != targetProductVer.Minor) return false; + if (thisProductVer.Build != targetProductVer.Build) return false; + } + else if ((validateFlags & (int) TransformValidations.MinorVersion) != 0) + { + if (thisProductVer.Major != targetProductVer.Major) return false; + if (thisProductVer.Minor != targetProductVer.Minor) return false; + } + else if ((validateFlags & (int) TransformValidations.MajorVersion) != 0) + { + if (thisProductVer.Major != targetProductVer.Major) return false; + } + + return true; + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/EmbeddedUIProxy.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/EmbeddedUIProxy.cs new file mode 100644 index 000000000..ca9163bc6 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/EmbeddedUIProxy.cs @@ -0,0 +1,234 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Reflection; + + /// + /// Managed-code portion of the embedded UI proxy. + /// + internal static class EmbeddedUIProxy + { + private static IEmbeddedUI uiInstance; + private static string uiClass; + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + private static bool DebugBreakEnabled(string method) + { + return CustomActionProxy.DebugBreakEnabled(new string[] { method, EmbeddedUIProxy.uiClass + "." + method } ); + } + + /// + /// Initializes managed embedded UI by loading the UI class and invoking its Initialize method. + /// + /// Integer handle to the installer session. + /// Name of the class that implements the embedded UI. This must + /// be of the form: "AssemblyName!Namespace.Class" + /// On entry, contains the current UI level for the installation. After this + /// method returns, the installer resets the UI level to the returned value of this parameter. + /// 0 if the embedded UI was successfully loaded and initialized, + /// ERROR_INSTALL_USEREXIT if the user canceled the installation during initialization, + /// or ERROR_INSTALL_FAILURE if the embedded UI could not be initialized. + /// + /// Due to interop limitations, the successful resulting UILevel is actually returned + /// as the high-word of the return value instead of via a ref parameter. + /// + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static int Initialize(int sessionHandle, string uiClass, int internalUILevel) + { + Session session = null; + + try + { + session = new Session((IntPtr) sessionHandle, false); + + if (string.IsNullOrWhiteSpace(uiClass)) + { + throw new ArgumentNullException("uiClass"); + } + + EmbeddedUIProxy.uiInstance = EmbeddedUIProxy.InstantiateUI(session, uiClass); + } + catch (Exception ex) + { + if (session != null) + { + try + { + session.Log("Exception while loading embedded UI:"); + session.Log(ex.ToString()); + } + catch (Exception) + { + } + } + } + + if (EmbeddedUIProxy.uiInstance == null) + { + return (int) ActionResult.Failure; + } + + try + { + string resourcePath = Path.GetDirectoryName(EmbeddedUIProxy.uiInstance.GetType().Assembly.Location); + InstallUIOptions uiOptions = (InstallUIOptions) internalUILevel; + if (EmbeddedUIProxy.DebugBreakEnabled("Initialize")) + { + System.Diagnostics.Debugger.Launch(); + } + + if (EmbeddedUIProxy.uiInstance.Initialize(session, resourcePath, ref uiOptions)) + { + // The embedded UI initialized and the installation should continue + // with internal UI reset according to options. + return ((int) uiOptions) << 16; + } + else + { + // The embedded UI did not initialize but the installation should still continue + // with internal UI reset according to options. + return (int) uiOptions; + } + } + catch (InstallCanceledException) + { + // The installation was canceled by the user. + return (int) ActionResult.UserExit; + } + catch (Exception ex) + { + // An unhandled exception causes the installation to fail immediately. + session.Log("Exception thrown by embedded UI initialization:"); + session.Log(ex.ToString()); + return (int) ActionResult.Failure; + } + } + + /// + /// Passes a progress message to the UI class. + /// + /// Installer message type and message box options. + /// Handle to a record containing message data. + /// Return value returned by the UI class. + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public static int ProcessMessage(int messageType, int recordHandle) + { + if (EmbeddedUIProxy.uiInstance != null) + { + try + { + int msgType = messageType & 0x7F000000; + int buttons = messageType & 0x0000000F; + int icon = messageType & 0x000000F0; + int defButton = messageType & 0x00000F00; + + Record msgRec = (recordHandle != 0 ? Record.FromHandle((IntPtr) recordHandle, false) : null); + using (msgRec) + { + if (EmbeddedUIProxy.DebugBreakEnabled("ProcessMessage")) + { + System.Diagnostics.Debugger.Launch(); + } + + return (int) EmbeddedUIProxy.uiInstance.ProcessMessage( + (InstallMessage) msgType, + msgRec, + (MessageButtons) buttons, + (MessageIcon) icon, + (MessageDefaultButton) defButton); + } + } + catch (Exception) + { + // Ignore it... just hope future messages will not throw exceptions. + } + } + + return 0; + } + + /// + /// Passes a shutdown message to the UI class. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public static void Shutdown() + { + if (EmbeddedUIProxy.uiInstance != null) + { + try + { + if (EmbeddedUIProxy.DebugBreakEnabled("Shutdown")) + { + System.Diagnostics.Debugger.Launch(); + } + + EmbeddedUIProxy.uiInstance.Shutdown(); + } + catch (Exception) + { + // Nothing to do at this point... the installation is done anyway. + } + + EmbeddedUIProxy.uiInstance = null; + } + } + + /// + /// Instantiates a UI class from a given assembly and class name. + /// + /// Installer session, for logging. + /// Name of the class that implements the embedded UI. This must + /// be of the form: "AssemblyName!Namespace.Class" + /// Interface on the UI class for handling UI messages. + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + private static IEmbeddedUI InstantiateUI(Session session, string uiClass) + { + int assemblySplit = uiClass.IndexOf('!'); + if (assemblySplit < 0) + { + session.Log("Error: invalid embedded UI assembly and class:" + uiClass); + return null; + } + + string assemblyName = uiClass.Substring(0, assemblySplit); + EmbeddedUIProxy.uiClass = uiClass.Substring(assemblySplit + 1); + + Assembly uiAssembly; + try + { + uiAssembly = AppDomain.CurrentDomain.Load(assemblyName); + + // This calls out to CustomActionProxy.DebugBreakEnabled() directly instead + // of calling EmbeddedUIProxy.DebugBreakEnabled() because we don't compose a + // class.method name for this breakpoint. + if (CustomActionProxy.DebugBreakEnabled(new string[] { "EmbeddedUI" })) + { + System.Diagnostics.Debugger.Launch(); + } + + return (IEmbeddedUI) uiAssembly.CreateInstance(EmbeddedUIProxy.uiClass); + } + catch (Exception ex) + { + session.Log("Error: could not load embedded UI class " + EmbeddedUIProxy.uiClass + " from assembly: " + assemblyName); + session.Log(ex.ToString()); + return null; + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Enums.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Enums.cs new file mode 100644 index 000000000..9881e8362 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Enums.cs @@ -0,0 +1,916 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Diagnostics.CodeAnalysis; + + // Enumerations are in alphabetical order. + + /// + /// Specifies a return status value for custom actions. + /// + public enum ActionResult : int + { + /// Action completed successfully. + Success = 0, + + /// Skip remaining actions, not an error. + SkipRemainingActions = 259, + + /// User terminated prematurely. + UserExit = 1602, + + /// Unrecoverable error or unhandled exception occurred. + Failure = 1603, + + /// Action not executed. + NotExecuted = 1626, + } + + /// + /// Specifies the open mode for a . + /// + public enum DatabaseOpenMode : int + { + /// Open a database read-only, no persistent changes. + ReadOnly = 0, + + /// Open a database read/write in transaction mode. + Transact = 1, + + /// Open a database direct read/write without transaction. + Direct = 2, + + /// Create a new database, transact mode read/write. + Create = 3, + + /// Create a new database, direct mode read/write. + CreateDirect = 4, + } + + /// + /// Log modes available for + /// and . + /// + [Flags] + public enum InstallLogModes : int + { + /// Disable logging. + None = 0, + + /// Log out of memory or fatal exit information. + FatalExit = (1 << ((int) InstallMessage.FatalExit >> 24)), + + /// Log error messages. + Error = (1 << ((int) InstallMessage.Error >> 24)), + + /// Log warning messages. + Warning = (1 << ((int) InstallMessage.Warning >> 24)), + + /// Log user requests. + User = (1 << ((int) InstallMessage.User >> 24)), + + /// Log status messages that are not displayed. + Info = (1 << ((int) InstallMessage.Info >> 24)), + + /// Log request to determine a valid source location. + ResolveSource = (1 << ((int) InstallMessage.ResolveSource >> 24)), + + /// Log insufficient disk space error. + OutOfDiskSpace = (1 << ((int) InstallMessage.OutOfDiskSpace >> 24)), + + /// Log the start of installation actions. + ActionStart = (1 << ((int) InstallMessage.ActionStart >> 24)), + + /// Log the data record for installation actions. + ActionData = (1 << ((int) InstallMessage.ActionData >> 24)), + + /// Log parameters for user-interface initialization. + CommonData = (1 << ((int) InstallMessage.CommonData >> 24)), + + /// Log the property values at termination. + PropertyDump = (1 << ((int) InstallMessage.Progress >> 24)), // log only + + /// + /// Sends large amounts of information to log file not generally useful to users. + /// May be used for support. + /// + Verbose = (1 << ((int) InstallMessage.Initialize >> 24)), // log only + + /// + /// Log extra debugging information. + /// + ExtraDebug = (1 << ((int) InstallMessage.Terminate >> 24)), // log only + + /// + /// Log only on error. + /// + LogOnlyOnError = (1 << ((int) InstallMessage.ShowDialog >> 24)), // log only + + /// + /// Log progress bar information. This message includes information on units so far and total number + /// of units. See for an explanation of the message format. This message + /// is only sent to an external user interface and is not logged. + /// + Progress = (1 << ((int) InstallMessage.Progress >> 24)), // external handler only + + /// + /// If this is not a quiet installation, then the basic UI has been initialized. If this is a full + /// UI installation, the Full UI is not yet initialized. This message is only sent to an external + /// user interface and is not logged. + /// + Initialize = (1 << ((int) InstallMessage.Initialize >> 24)), // external handler only + + /// + /// If a full UI is being used, the full UI has ended. If this is not a quiet installation, the basic + /// UI has not yet ended. This message is only sent to an external user interface and is not logged. + /// + Terminate = (1 << ((int) InstallMessage.Terminate >> 24)), // external handler only + + /// + /// Sent prior to display of the Full UI dialog. This message is only sent to an external user + /// interface and is not logged. + /// + ShowDialog = (1 << ((int) InstallMessage.ShowDialog >> 24)), // external handler only + + /// + /// List of files in use that need to be replaced. + /// + FilesInUse = (1 << ((int) InstallMessage.FilesInUse >> 24)), // external handler only + + /// + /// [MSI 4.0] List of apps that the user can request Restart Manager to shut down and restart. + /// + RMFilesInUse = (1 << ((int) InstallMessage.RMFilesInUse >> 24)), // external handler only + } + + /// + /// Type of message to be processed by , + /// , or . + /// + [SuppressMessage("Microsoft.Design", "CA1027:MarkEnumsWithFlags")] + public enum InstallMessage : int + { + /// Premature termination, possibly fatal OOM. + FatalExit = 0x00000000, + + /// Formatted error message. + Error = 0x01000000, + + /// Formatted warning message. + Warning = 0x02000000, + + /// User request message. + User = 0x03000000, + + /// Informative message for log. + Info = 0x04000000, + + /// List of files in use that need to be replaced. + FilesInUse = 0x05000000, + + /// Request to determine a valid source location. + ResolveSource = 0x06000000, + + /// Insufficient disk space message. + OutOfDiskSpace = 0x07000000, + + /// Start of action: action name & description. + ActionStart = 0x08000000, + + /// Formatted data associated with individual action item. + ActionData = 0x09000000, + + /// Progress gauge info: units so far, total. + Progress = 0x0A000000, + + /// Product info for dialog: language Id, dialog caption. + CommonData = 0x0B000000, + + /// Sent prior to UI initialization, no string data. + Initialize = 0x0C000000, + + /// Sent after UI termination, no string data. + Terminate = 0x0D000000, + + /// Sent prior to display or authored dialog or wizard. + ShowDialog = 0x0E000000, + + /// [MSI 4.0] List of apps that the user can request Restart Manager to shut down and restart. + RMFilesInUse = 0x19000000, + + /// [MSI 4.5] Sent prior to install of a product. + InstallStart = 0x1A000000, + + /// [MSI 4.5] Sent after install of a product. + InstallEnd = 0x1B000000, + } + + /// + /// Specifies the install mode for or . + /// + public enum InstallMode : int + { + /// Provide the component only if the feature's installation state is . + NoSourceResolution = -3, + + /// Only check that the component is registered, without verifying that the key file of the component exists. + NoDetection = -2, + + /// Provide the component only if the feature exists. + Existing = -1, + + /// Provide the component and perform any installation necessary to provide the component. + Default = 0, + } + + /// + /// Specifies the run mode for . + /// + [SuppressMessage("Microsoft.Design", "CA1027:MarkEnumsWithFlags")] + public enum InstallRunMode : int + { + /// The administrative mode is installing, or the product is installing. + Admin = 0, + + /// The advertisements are installing or the product is installing or updating. + Advertise = 1, + + /// An existing installation is being modified or there is a new installation. + Maintenance = 2, + + /// Rollback is enabled. + RollbackEnabled = 3, + + /// The log file is active. It was enabled prior to the installation session. + LogEnabled = 4, + + /// Execute operations are spooling or they are in the determination phase. + Operations = 5, + + /// A reboot is necessary after a successful installation (settable). + RebootAtEnd = 6, + + /// A reboot is necessary to continue the installation (settable). + RebootNow = 7, + + /// Files from cabinets and Media table files are installing. + Cabinet = 8, + + /// The source LongFileNames is suppressed through the PID_MSISOURCE summary property. + SourceShortNames = 9, + + /// The target LongFileNames is suppressed through the SHORTFILENAMES property. + TargetShortNames = 10, + + // Reserved for future use. + //Reserved11 = 11, + + /// The operating system is Windows 95, Windows 98, or Windows ME. + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "x")] + Windows9x = 12, + + /// The operating system supports demand installation. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Zaw")] + ZawEnabled = 13, + + // Reserved for future use. + //Reserved14 = 14, + + // Reserved for future use. + //Reserved15 = 15, + + /// A custom action called from install script execution. + Scheduled = 16, + + /// A custom action called from rollback execution script. + Rollback = 17, + + /// A custom action called from commit execution script. + Commit = 18, + } + + /// + /// Installed state of a Component or Feature. + /// + public enum InstallState : int + { + /// The component is disabled. + NotUsed = -7, + + /// The installation configuration data is corrupt. + BadConfig = -6, + + /// The installation is suspended or in progress. + Incomplete = -5, + + /// Component is set to run from source, but source is unavailable. + SourceAbsent = -4, + + /// The buffer overflow is returned. + MoreData = -3, + + /// An invalid parameter was passed to the function. + InvalidArgument = -2, + + /// An unrecognized product or feature name was passed to the function. + Unknown = -1, + + /// The component is broken. + Broken = 0, + + /// The feature is advertised. + Advertised = 1, + + /// The component is being removed. In action state and not settable. + Removed = 1, + + /// The component is not installed, or action state is absent but clients remain. + Absent = 2, + + /// The component is installed on the local drive. + Local = 3, + + /// The component will run from the source, CD, or network. + Source = 4, + + /// The component will be installed in the default location: local or source. + Default = 5, + } + + /// + /// Specifies the type of installation for . + /// + public enum InstallType : int + { + /// Searches system for products to patch. + Default = 0, + + /// Indicates a administrative installation. + NetworkImage = 1, + + /// Indicates a particular instance. + SingleInstance = 2, + } + + /// + /// Level of the installation user interface, specified with + /// . + /// + [SuppressMessage("Microsoft.Design", "CA1008:EnumsShouldHaveZeroValue")] + [Flags] + public enum InstallUIOptions : int + { + /// Does not change UI level. + NoChange = 0, + + /// Uses Default UI level. + Default = 1, + + /// Silent installation. + Silent = 2, + + /// Simple progress and error handling. + Basic = 3, + + /// Authored UI, wizard dialogs suppressed. + Reduced = 4, + + /// Authored UI with wizards, progress, and errors. + Full = 5, + + /// + /// When combined with the value, the installer does not display + /// the cancel button in the progress dialog. + /// + HideCancel = 0x20, + + /// + /// When combined with the value, the installer displays progress + /// dialog boxes but does not display any modal dialog boxes or error dialog boxes. + /// + ProgressOnly = 0x40, + + /// + /// When combined with another value, the installer displays a modal dialog + /// box at the end of a successful installation or if there has been an error. + /// No dialog box is displayed if the user cancels. + /// + EndDialog = 0x80, + + /// + /// Forces display of the source resolution dialog even if the UI is otherwise silent. + /// + SourceResolutionOnly = 0x100, + + /// + /// [MSI 5.0] Forces display of the UAC dialog even if the UI is otherwise silent. + /// + UacOnly = 0x200, + } + + /// + /// Specifies a return status value for message handlers. These values are returned by + /// , , and . + /// + public enum MessageResult : int + { + /// An error was found in the message handler. + Error = -1, + + /// No action was taken. + None = 0, + + /// IDOK + [SuppressMessage("Microsoft.Naming", "CA1706:ShortAcronymsShouldBeUppercase")] + OK = 1, + + /// IDCANCEL + Cancel = 2, + + /// IDABORT + Abort = 3, + + /// IDRETRY + Retry = 4, + + /// IDIGNORE + Ignore = 5, + + /// IDYES + Yes = 6, + + /// IDNO + No = 7, + } + + /// + /// Specifies constants defining which buttons to display for a message. This can be cast to + /// the MessageBoxButtons enum in System.Windows.Forms and System.Windows. + /// + public enum MessageButtons + { + /// + /// The message contains an OK button. + /// + OK = 0, + + /// + /// The message contains OK and Cancel buttons. + /// + OKCancel = 1, + + /// + /// The message contains Abort, Retry, and Ignore buttons. + /// + AbortRetryIgnore = 2, + + /// + /// The message contains Yes, No, and Cancel buttons. + /// + YesNoCancel = 3, + + /// + /// The message contains Yes and No buttons. + /// + YesNo = 4, + + /// + /// The message contains Retry and Cancel buttons. + /// + RetryCancel = 5, + } + + /// + /// Specifies constants defining which information to display. This can be cast to + /// the MessageBoxIcon enum in System.Windows.Forms and System.Windows. + /// + public enum MessageIcon + { + /// + /// The message contain no symbols. + /// + None = 0, + + /// + /// The message contains a symbol consisting of white X in a circle with a red background. + /// + Error = 16, + + /// + /// The message contains a symbol consisting of a white X in a circle with a red background. + /// + Hand = 16, + + /// + /// The message contains a symbol consisting of white X in a circle with a red background. + /// + Stop = 16, + + /// + /// The message contains a symbol consisting of a question mark in a circle. + /// + Question = 32, + + /// + /// The message contains a symbol consisting of an exclamation point in a triangle with a yellow background. + /// + Exclamation = 48, + + /// + /// The message contains a symbol consisting of an exclamation point in a triangle with a yellow background. + /// + Warning = 48, + + /// + /// The message contains a symbol consisting of a lowercase letter i in a circle. + /// + Information = 64, + + /// + /// The message contains a symbol consisting of a lowercase letter i in a circle. + /// + Asterisk = 64, + } + + /// + /// Specifies constants defining the default button on a message. This can be cast to + /// the MessageBoxDefaultButton enum in System.Windows.Forms and System.Windows. + /// + public enum MessageDefaultButton + { + /// + /// The first button on the message is the default button. + /// + Button1 = 0, + + /// + /// The second button on the message is the default button. + /// + Button2 = 256, + + /// + /// The third button on the message is the default button. + /// + Button3 = 512, + } + + /// + /// Additional styles for use with message boxes. + /// + [Flags] + internal enum MessageBoxStyles + { + /// + /// The message box is created with the WS_EX_TOPMOST window style. + /// + TopMost = 0x00040000, + + /// + /// The caller is a service notifying the user of an event. + /// The function displays a message box on the current active desktop, even if there is no user logged on to the computer. + /// + ServiceNotification = 0x00200000, + } + + /// + /// Specifies the different patch states for . + /// + [Flags] + public enum PatchStates : int + { + /// Invalid value. + None = 0, + + /// Patches applied to a product. + Applied = 1, + + /// Patches that are superseded by other patches. + Superseded = 2, + + /// Patches that are obsolesced by other patches. + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Obsoleted")] + Obsoleted = 4, + + /// Patches that are registered to a product but not applied. + Registered = 8, + + /// All valid patch states. + All = (Applied | Superseded | Obsoleted | Registered) + } + + /// + /// Specifies the reinstall mode for or . + /// + [Flags] + public enum ReinstallModes : int + { + /// Reinstall only if file is missing. + FileMissing = 0x00000002, + + /// Reinstall if file is missing, or older version. + FileOlderVersion = 0x00000004, + + /// Reinstall if file is missing, or equal or older version. + FileEqualVersion = 0x00000008, + + /// Reinstall if file is missing, or not exact version. + FileExact = 0x00000010, + + /// Checksum executables, reinstall if missing or corrupt. + FileVerify = 0x00000020, + + /// Reinstall all files, regardless of version. + FileReplace = 0x00000040, + + /// Insure required machine reg entries. + MachineData = 0x00000080, + + /// Insure required user reg entries. + UserData = 0x00000100, + + /// Validate shortcuts items. + Shortcut = 0x00000200, + + /// Use re-cache source install package. + Package = 0x00000400, + } + + /// + /// Attributes for methods. + /// + [Flags] + public enum TransactionAttributes : int + { + /// No attributes. + None = 0x00000000, + + /// Request that the Windows Installer not shutdown the embedded UI until the transaction is complete. + ChainEmbeddedUI = 0x00000001, + + /// Request that the Windows Installer transfer the embedded UI from the original installation. + JoinExistingEmbeddedUI = 0x00000002, + } + + /// + /// Transform error conditions available for or + /// . + /// + [Flags] + public enum TransformErrors : int + { + /// No error conditions. + None = 0x0000, + + /// Adding a row that already exists. + AddExistingRow = 0x0001, + + /// Deleting a row that doesn't exist. + DelMissingRow = 0x0002, + + /// Adding a table that already exists. + AddExistingTable = 0x0004, + + /// Deleting a table that doesn't exist. + DelMissingTable = 0x0008, + + /// Updating a row that doesn't exist. + UpdateMissingRow = 0x0010, + + /// Transform and database code pages do not match and neither code page is neutral. + ChangeCodePage = 0x0020, + + /// Create the temporary _TransformView table when applying the transform. + ViewTransform = 0x0100, + } + + /// + /// Transform validation flags available for . + /// + [Flags] + public enum TransformValidations : int + { + /// Validate no properties. + None = 0x0000, + + /// Default language must match base database. + Language = 0x0001, + + /// Product must match base database. + Product = 0x0002, + + /// Check major version only. + MajorVersion = 0x0008, + + /// Check major and minor versions only. + MinorVersion = 0x0010, + + /// Check major, minor, and update versions. + UpdateVersion = 0x0020, + + /// Installed version < base version. + NewLessBaseVersion = 0x0040, + + /// Installed version <= base version. + NewLessEqualBaseVersion = 0x0080, + + /// Installed version = base version. + NewEqualBaseVersion = 0x0100, + + /// Installed version >= base version. + NewGreaterEqualBaseVersion = 0x0200, + + /// Installed version > base version. + NewGreaterBaseVersion = 0x0400, + + /// UpgradeCode must match base database. + UpgradeCode = 0x0800, + } + + /// + /// Specifies the installation context for s, + /// es, and + /// + /// + [Flags] + public enum UserContexts : int + { + /// Not installed. + None = 0, + + /// User managed install context. + UserManaged = 1, + + /// User non-managed context. + UserUnmanaged = 2, + + /// Per-machine context. + Machine = 4, + + /// All contexts, or all valid values. + All = (UserManaged | UserUnmanaged | Machine), + + /// All user-managed contexts. + AllUserManaged = 8, + } + + /// + /// Defines the type of error encountered by the , , + /// or methods of the class. + /// + public enum ValidationError : int + { + /* + InvalidArg = -3, + MoreData = -2, + FunctionError = -1, + */ + + /// No error. + None = 0, + + /// The new record duplicates primary keys of the existing record in a table. + DuplicateKey = 1, + + /// There are no null values allowed, or the column is about to be deleted but is referenced by another row. + Required = 2, + + /// The corresponding record in a foreign table was not found. + BadLink = 3, + + /// The data is greater than the maximum value allowed. + Overflow = 4, + + /// The data is less than the minimum value allowed. + Underflow = 5, + + /// The data is not a member of the values permitted in the set. + NotInSet = 6, + + /// An invalid version string was supplied. + BadVersion = 7, + + /// The case was invalid. The case must be all uppercase or all lowercase. + BadCase = 8, + + /// An invalid GUID was supplied. + BadGuid = 9, + + /// An invalid wildcard file name was supplied, or the use of wildcards was invalid. + BadWildcard = 10, + + /// An invalid identifier was supplied. + BadIdentifier = 11, + + /// Invalid language IDs were supplied. + BadLanguage = 12, + + /// An invalid file name was supplied. + BadFileName = 13, + + /// An invalid path was supplied. + BadPath = 14, + + /// An invalid conditional statement was supplied. + BadCondition = 15, + + /// An invalid format string was supplied. + BadFormatted = 16, + + /// An invalid template string was supplied. + BadTemplate = 17, + + /// An invalid string was supplied in the DefaultDir column of the Directory table. + BadDefaultDir = 18, + + /// An invalid registry path string was supplied. + BadRegPath = 19, + + /// An invalid string was supplied in the CustomSource column of the CustomAction table. + BadCustomSource = 20, + + /// An invalid property string was supplied. + BadProperty = 21, + + /// The _Validation table is missing a reference to a column. + MissingData = 22, + + /// The category column of the _Validation table for the column is invalid. + BadCategory = 23, + + /// The table in the Keytable column of the _Validation table was not found or loaded. + BadKeyTable = 24, + + /// The value in the MaxValue column of the _Validation table is less than the value in the MinValue column. + BadMaxMinValues = 25, + + /// An invalid cabinet name was supplied. + BadCabinet = 26, + + /// An invalid shortcut target name was supplied. + BadShortcut = 27, + + /// The string is too long for the length specified by the column definition. + StringOverflow = 28, + + /// An invalid localization attribute was supplied. (Primary keys cannot be localized.) + BadLocalizeAttrib = 29 + } + + /// + /// Specifies the modify mode for . + /// + public enum ViewModifyMode : int + { + /// + /// Refreshes the information in the supplied record without changing the position + /// in the result set and without affecting subsequent fetch operations. + /// + Seek = -1, + + /// Refreshes the data in a Record. + Refresh = 0, + + /// Inserts a Record into the view. + Insert = 1, + + /// Updates the View with new data from the Record. + Update = 2, + + /// Updates or inserts a Record into the View. + Assign = 3, + + /// Updates or deletes and inserts a Record into the View. + Replace = 4, + + /// Inserts or validates a record. + Merge = 5, + + /// Deletes a Record from the View. + Delete = 6, + + /// Inserts a Record into the View. The inserted data is not persistent. + InsertTemporary = 7, + + /// Validates a record. + Validate = 8, + + /// Validates a new record. + [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")] + ValidateNew = 9, + + /// Validates fields of a fetched or new record. Can validate one or more fields of an incomplete record. + ValidateField = 10, + + /// Validates a record that will be deleted later. + ValidateDelete = 11, + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Exceptions.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Exceptions.cs new file mode 100644 index 000000000..e1e7283f3 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Exceptions.cs @@ -0,0 +1,589 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Exceptions for the Microsoft.Deployment.WindowsInstaller namespace. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Runtime.Serialization; + using System.Security.Permissions; + using System.Text; + + /// + /// Base class for Windows Installer exceptions. + /// + [Serializable] + internal class InstallerException : SystemException + { + private int errorCode; + private object[] errorData; + + /// + /// Creates a new InstallerException with a specified error message and a reference to the + /// inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. If the + /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception + /// is raised in a catch block that handles the inner exception. + public InstallerException(string msg, Exception innerException) + : this(0, msg, innerException) + { + } + + /// + /// Creates a new InstallerException with a specified error message. + /// + /// The message that describes the error. + public InstallerException(string msg) + : this(0, msg) + { + } + + /// + /// Creates a new InstallerException. + /// + public InstallerException() + : this(0, null) + { + } + + internal InstallerException(int errorCode, string msg, Exception innerException) + : base(msg, innerException) + { + this.errorCode = errorCode; + this.SaveErrorRecord(); + } + + internal InstallerException(int errorCode, string msg) + : this(errorCode, msg, null) + { + } + + /// + /// Initializes a new instance of the InstallerException class with serialized data. + /// + /// The SerializationInfo that holds the serialized object data about the exception being thrown. + /// The StreamingContext that contains contextual information about the source or destination. + protected InstallerException(SerializationInfo info, StreamingContext context) : base(info, context) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + + this.errorCode = info.GetInt32("msiErrorCode"); + } + + /// + /// Gets the system error code that resulted in this exception, or 0 if not applicable. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public int ErrorCode + { + get + { + return this.errorCode; + } + } + + /// + /// Gets a message that describes the exception. This message may contain detailed + /// formatted error data if it was available. + /// + public override String Message + { + get + { + string msg = base.Message; + using (Record errorRec = this.GetErrorRecord()) + { + if (errorRec != null) + { + string errorMsg = Installer.GetErrorMessage(errorRec, CultureInfo.InvariantCulture); + msg = Combine(msg, errorMsg); + } + } + return msg; + } + } + + /// + /// Sets the SerializationInfo with information about the exception. + /// + /// The SerializationInfo that holds the serialized object data about the exception being thrown. + /// The StreamingContext that contains contextual information about the source or destination. + [SecurityPermission(SecurityAction.Demand, SerializationFormatter=true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + + info.AddValue("msiErrorCode", this.errorCode); + base.GetObjectData(info, context); + } + + /// + /// Gets extended information about the error, or null if no further information + /// is available. + /// + /// A Record object. Field 1 of the Record contains the installer + /// message code. Other fields contain data specific to the particular error. + ///

+ /// If the record is passed to , it is formatted + /// by looking up the string in the current database. If there is no installation + /// session, the formatted error message may be obtained by a query on the Error table using + /// the error code, followed by a call to . + /// Alternatively, the standard MSI message can by retrieved by calling the + /// method. + ///

+ /// The following methods and properties may report extended error data: + /// + /// (constructor) + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// (constructor) + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// . + /// + ///

+ /// The Record object should be d after use. + /// It is best that the handle be closed manually as soon as it is no longer + /// needed, as leaving lots of unused handles open can degrade performance. + ///

+ /// Win32 MSI API: + /// MsiGetLastErrorRecord + ///

+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] + public Record GetErrorRecord() + { + return this.errorData != null ? new Record(this.errorData) : null; + } + + internal static Exception ExceptionFromReturnCode(uint errorCode) + { + return ExceptionFromReturnCode(errorCode, null); + } + + internal static Exception ExceptionFromReturnCode(uint errorCode, string msg) + { + msg = Combine(GetSystemMessage(errorCode), msg); + switch (errorCode) + { + case (uint) NativeMethods.Error.FILE_NOT_FOUND: + case (uint) NativeMethods.Error.PATH_NOT_FOUND: return new FileNotFoundException(msg); + + case (uint) NativeMethods.Error.INVALID_PARAMETER: + case (uint) NativeMethods.Error.DIRECTORY: + case (uint) NativeMethods.Error.UNKNOWN_PROPERTY: + case (uint) NativeMethods.Error.UNKNOWN_PRODUCT: + case (uint) NativeMethods.Error.UNKNOWN_FEATURE: + case (uint) NativeMethods.Error.UNKNOWN_COMPONENT: return new ArgumentException(msg); + + case (uint) NativeMethods.Error.BAD_QUERY_SYNTAX: return new BadQuerySyntaxException(msg); + + case (uint) NativeMethods.Error.INVALID_HANDLE_STATE: + case (uint) NativeMethods.Error.INVALID_HANDLE: + InvalidHandleException ihex = new InvalidHandleException(msg); + ihex.errorCode = (int) errorCode; + return ihex; + + case (uint) NativeMethods.Error.INSTALL_USEREXIT: return new InstallCanceledException(msg); + + case (uint) NativeMethods.Error.CALL_NOT_IMPLEMENTED: return new NotImplementedException(msg); + + default: return new InstallerException((int) errorCode, msg); + } + } + + internal static string GetSystemMessage(uint errorCode) + { + const uint FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200; + const uint FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000; + + StringBuilder buf = new StringBuilder(1024); + uint formatCount = NativeMethods.FormatMessage( + FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + IntPtr.Zero, + (uint) errorCode, + 0, + buf, + (uint) buf.Capacity, + IntPtr.Zero); + + if (formatCount != 0) + { + return buf.ToString().Trim(); + } + else + { + return null; + } + } + + internal void SaveErrorRecord() + { + // TODO: pass an affinity handle here? + int recordHandle = RemotableNativeMethods.MsiGetLastErrorRecord(0); + if (recordHandle != 0) + { + using (Record errorRec = new Record((IntPtr) recordHandle, true, null)) + { + this.errorData = new object[errorRec.FieldCount]; + for (int i = 0; i < this.errorData.Length; i++) + { + this.errorData[i] = errorRec[i + 1]; + } + } + } + else + { + this.errorData = null; + } + } + + private static string Combine(string msg1, string msg2) + { + if (msg1 == null) return msg2; + if (msg2 == null) return msg1; + return msg1 + " " + msg2; + } + } + + /// + /// User Canceled the installation. + /// + [Serializable] + internal class InstallCanceledException : InstallerException + { + /// + /// Creates a new InstallCanceledException with a specified error message and a reference to the + /// inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. If the + /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception + /// is raised in a catch block that handles the inner exception. + public InstallCanceledException(string msg, Exception innerException) + : base((int) NativeMethods.Error.INSTALL_USEREXIT, msg, innerException) + { + } + + /// + /// Creates a new InstallCanceledException with a specified error message. + /// + /// The message that describes the error. + public InstallCanceledException(string msg) + : this(msg, null) + { + } + + /// + /// Creates a new InstallCanceledException. + /// + public InstallCanceledException() + : this(null, null) + { + } + + /// + /// Initializes a new instance of the InstallCanceledException class with serialized data. + /// + /// The SerializationInfo that holds the serialized object data about the exception being thrown. + /// The StreamingContext that contains contextual information about the source or destination. + protected InstallCanceledException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } + + /// + /// A bad SQL query string was passed to or . + /// + [Serializable] + internal class BadQuerySyntaxException : InstallerException + { + /// + /// Creates a new BadQuerySyntaxException with a specified error message and a reference to the + /// inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. If the + /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception + /// is raised in a catch block that handles the inner exception. + public BadQuerySyntaxException(string msg, Exception innerException) + : base((int) NativeMethods.Error.BAD_QUERY_SYNTAX, msg, innerException) + { + } + + /// + /// Creates a new BadQuerySyntaxException with a specified error message. + /// + /// The message that describes the error. + public BadQuerySyntaxException(string msg) + : this(msg, null) + { + } + + /// + /// Creates a new BadQuerySyntaxException. + /// + public BadQuerySyntaxException() + : this(null, null) + { + } + + /// + /// Initializes a new instance of the BadQuerySyntaxException class with serialized data. + /// + /// The SerializationInfo that holds the serialized object data about the exception being thrown. + /// The StreamingContext that contains contextual information about the source or destination. + protected BadQuerySyntaxException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } + + /// + /// A method was called on an invalid installer handle. The handle may have been already closed. + /// + [Serializable] + internal class InvalidHandleException : InstallerException + { + /// + /// Creates a new InvalidHandleException with a specified error message and a reference to the + /// inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. If the + /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception + /// is raised in a catch block that handles the inner exception. + public InvalidHandleException(string msg, Exception innerException) + : base((int) NativeMethods.Error.INVALID_HANDLE, msg, innerException) + { + } + + /// + /// Creates a new InvalidHandleException with a specified error message. + /// + /// The message that describes the error. + public InvalidHandleException(string msg) + : this(msg, null) + { + } + + /// + /// Creates a new InvalidHandleException. + /// + public InvalidHandleException() + : this(null, null) + { + } + + /// + /// Initializes a new instance of the InvalidHandleException class with serialized data. + /// + /// The SerializationInfo that holds the serialized object data about the exception being thrown. + /// The StreamingContext that contains contextual information about the source or destination. + protected InvalidHandleException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } + + /// + /// A failure occurred when executing . The exception may contain + /// details about the merge conflict. + /// + [Serializable] + internal class MergeException : InstallerException + { + private IList conflictTables; + private IList conflictCounts; + + /// + /// Creates a new MergeException with a specified error message and a reference to the + /// inner exception that is the cause of this exception. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. If the + /// innerException parameter is not a null reference (Nothing in Visual Basic), the current exception + /// is raised in a catch block that handles the inner exception. + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public MergeException(string msg, Exception innerException) + : base(msg, innerException) + { + } + + /// + /// Creates a new MergeException with a specified error message. + /// + /// The message that describes the error. + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public MergeException(string msg) + : base(msg) + { + } + + /// + /// Creates a new MergeException. + /// + public MergeException() + : base() + { + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal MergeException(Database db, string conflictsTableName) + : base("Merge failed.") + { + if (conflictsTableName != null) + { + IList conflictTableList = new List(); + IList conflictCountList = new List(); + + using (View view = db.OpenView("SELECT `Table`, `NumRowMergeConflicts` FROM `" + conflictsTableName + "`")) + { + view.Execute(); + + foreach (Record rec in view) using (rec) + { + conflictTableList.Add(rec.GetString(1)); + conflictCountList.Add((int) rec.GetInteger(2)); + } + } + + this.conflictTables = conflictTableList; + this.conflictCounts = conflictCountList; + } + } + + /// + /// Initializes a new instance of the MergeException class with serialized data. + /// + /// The SerializationInfo that holds the serialized object data about the exception being thrown. + /// The StreamingContext that contains contextual information about the source or destination. + protected MergeException(SerializationInfo info, StreamingContext context) : base(info, context) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + + this.conflictTables = (string[]) info.GetValue("mergeConflictTables", typeof(string[])); + this.conflictCounts = (int[]) info.GetValue("mergeConflictCounts", typeof(int[])); + } + + /// + /// Gets the number of merge conflicts in each table, corresponding to the tables returned by + /// . + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public IList ConflictCounts + { + get + { + return new List(this.conflictCounts); + } + } + + /// + /// Gets the list of tables containing merge conflicts. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public IList ConflictTables + { + get + { + return new List(this.conflictTables); + } + } + + /// + /// Gets a message that describes the merge conflits. + /// + public override String Message + { + get + { + StringBuilder msg = new StringBuilder(base.Message); + if (this.conflictTables != null) + { + for (int i = 0; i < this.conflictTables.Count; i++) + { + msg.Append(i == 0 ? " Conflicts: " : ", "); + msg.Append(this.conflictTables[i]); + msg.Append('('); + msg.Append(this.conflictCounts[i]); + msg.Append(')'); + } + } + return msg.ToString(); + } + } + + /// + /// Sets the SerializationInfo with information about the exception. + /// + /// The SerializationInfo that holds the serialized object data about the exception being thrown. + /// The StreamingContext that contains contextual information about the source or destination. + [SecurityPermission(SecurityAction.Demand, SerializationFormatter=true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + { + throw new ArgumentNullException("info"); + } + + info.AddValue("mergeConflictTables", this.conflictTables); + info.AddValue("mergeConflictCounts", this.conflictCounts); + base.GetObjectData(info, context); + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ExternalUIHandler.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ExternalUIHandler.cs new file mode 100644 index 000000000..d46012728 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ExternalUIHandler.cs @@ -0,0 +1,231 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Microsoft.Deployment.WindowsInstaller.ExternalUIHandler and related delegates, +// with related parts of Installer class. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections; + using System.Diagnostics.CodeAnalysis; + using System.Runtime.InteropServices; + + /// + /// Defines a callback function that the installer calls for progress notification and error messages. + /// + internal delegate MessageResult ExternalUIHandler( + InstallMessage messageType, + string message, + MessageButtons buttons, + MessageIcon icon, + MessageDefaultButton defaultButton); + + /// + /// [MSI 3.1] Defines a callback function that the installer calls for record-based progress notification and error messages. + /// + internal delegate MessageResult ExternalUIRecordHandler( + InstallMessage messageType, + Record messageRecord, + MessageButtons buttons, + MessageIcon icon, + MessageDefaultButton defaultButton); + + internal delegate int NativeExternalUIHandler(IntPtr context, int messageType, [MarshalAs(UnmanagedType.LPWStr)] string message); + + internal delegate int NativeExternalUIRecordHandler(IntPtr context, int messageType, int recordHandle); + + internal class ExternalUIProxy + { + private ExternalUIHandler handler; + + internal ExternalUIProxy(ExternalUIHandler handler) + { + this.handler = handler; + } + + public ExternalUIHandler Handler + { + get { return this.handler; } + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public int ProxyHandler(IntPtr contextPtr, int messageType, [MarshalAs(UnmanagedType.LPWStr)] string message) + { + try + { + int msgType = messageType & 0x7F000000; + int buttons = messageType & 0x0000000F; + int icon = messageType & 0x000000F0; + int defButton = messageType & 0x00000F00; + + return (int) this.handler( + (InstallMessage) msgType, + message, + (MessageButtons) buttons, + (MessageIcon) icon, + (MessageDefaultButton) defButton); + } + catch + { + return (int) MessageResult.Error; + } + } + } + + internal class ExternalUIRecordProxy + { + private ExternalUIRecordHandler handler; + + internal ExternalUIRecordProxy(ExternalUIRecordHandler handler) + { + this.handler = handler; + } + + public ExternalUIRecordHandler Handler + { + get { return this.handler; } + } + + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] + public int ProxyHandler(IntPtr contextPtr, int messageType, int recordHandle) + { + try + { + int msgType = messageType & 0x7F000000; + int buttons = messageType & 0x0000000F; + int icon = messageType & 0x000000F0; + int defButton = messageType & 0x00000F00; + + Record msgRec = (recordHandle != 0 ? Record.FromHandle((IntPtr) recordHandle, false) : null); + using (msgRec) + { + return (int) this.handler( + (InstallMessage) msgType, + msgRec, + (MessageButtons) buttons, + (MessageIcon) icon, + (MessageDefaultButton) defButton); + } + } + catch + { + return (int) MessageResult.Error; + } + } + } + + internal static partial class Installer + { + private static IList externalUIHandlers = ArrayList.Synchronized(new ArrayList()); + + /// + /// Enables an external user-interface handler. This external UI handler is called before the + /// normal internal user-interface handler. The external UI handler has the option to suppress + /// the internal UI by returning a non-zero value to indicate that it has handled the messages. + /// + /// A callback delegate that handles the UI messages + /// Specifies which messages to handle using the external message handler. + /// If the external handler returns a non-zero result, then that message will not be sent to the UI, + /// instead the message will be logged if logging has been enabled. + /// The previously set external handler, or null if there was no previously set handler + ///

+ /// To restore the previous UI handler, a second call is made to SetExternalUI using the + /// ExternalUIHandler returned by the first call to SetExternalUI and specifying + /// as the message filter. + ///

+ /// The external user interface handler does not have full control over the external user + /// interface unless is called with the uiLevel parameter set to + /// . If SetInternalUI is not called, the internal user + /// interface level defaults to . As a result, any message not + /// handled by the external user interface handler is handled by Windows Installer. The initial + /// "Preparing to install..." dialog always appears even if the external user interface + /// handler handles all messages. + ///

+ /// SetExternalUI should only be called from a bootstrapping application. You cannot call + /// it from a custom action + ///

+ /// Win32 MSI API: + /// MsiSetExternalUI + ///

+ internal static ExternalUIHandler SetExternalUI(ExternalUIHandler uiHandler, InstallLogModes messageFilter) + { + NativeExternalUIHandler nativeHandler = null; + if (uiHandler != null) + { + nativeHandler = new ExternalUIProxy(uiHandler).ProxyHandler; + Installer.externalUIHandlers.Add(nativeHandler); + } + NativeExternalUIHandler oldNativeHandler = NativeMethods.MsiSetExternalUI(nativeHandler, (uint) messageFilter, IntPtr.Zero); + if (oldNativeHandler != null && oldNativeHandler.Target is ExternalUIProxy) + { + Installer.externalUIHandlers.Remove(oldNativeHandler); + return ((ExternalUIProxy) oldNativeHandler.Target).Handler; + } + else + { + return null; + } + } + + /// + /// [MSI 3.1] Enables a record-based external user-interface handler. This external UI handler is called + /// before the normal internal user-interface handler. The external UI handler has the option to suppress + /// the internal UI by returning a non-zero value to indicate that it has handled the messages. + /// + /// A callback delegate that handles the UI messages + /// Specifies which messages to handle using the external message handler. + /// If the external handler returns a non-zero result, then that message will not be sent to the UI, + /// instead the message will be logged if logging has been enabled. + /// The previously set external handler, or null if there was no previously set handler + ///

+ /// To restore the previous UI handler, a second call is made to SetExternalUI using the + /// ExternalUIHandler returned by the first call to SetExternalUI and specifying + /// as the message filter. + ///

+ /// The external user interface handler does not have full control over the external user + /// interface unless is called with the uiLevel parameter set to + /// . If SetInternalUI is not called, the internal user + /// interface level defaults to . As a result, any message not + /// handled by the external user interface handler is handled by Windows Installer. The initial + /// "Preparing to install..." dialog always appears even if the external user interface + /// handler handles all messages. + ///

+ /// SetExternalUI should only be called from a bootstrapping application. You cannot call + /// it from a custom action + ///

+ /// Win32 MSI API: + /// MsiSetExternalUIRecord + ///

+ internal static ExternalUIRecordHandler SetExternalUI(ExternalUIRecordHandler uiHandler, InstallLogModes messageFilter) + { + NativeExternalUIRecordHandler nativeHandler = null; + if (uiHandler != null) + { + nativeHandler = new ExternalUIRecordProxy(uiHandler).ProxyHandler; + Installer.externalUIHandlers.Add(nativeHandler); + } + NativeExternalUIRecordHandler oldNativeHandler; + uint ret = NativeMethods.MsiSetExternalUIRecord(nativeHandler, (uint) messageFilter, IntPtr.Zero, out oldNativeHandler); + if (ret != 0) + { + Installer.externalUIHandlers.Remove(nativeHandler); + throw InstallerException.ExceptionFromReturnCode(ret); + } + + if (oldNativeHandler != null && oldNativeHandler.Target is ExternalUIRecordProxy) + { + Installer.externalUIHandlers.Remove(oldNativeHandler); + return ((ExternalUIRecordProxy) oldNativeHandler.Target).Handler; + } + else + { + return null; + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/FeatureInfo.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/FeatureInfo.cs new file mode 100644 index 000000000..b9617ea41 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/FeatureInfo.cs @@ -0,0 +1,515 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Text; + + /// + /// Accessor for information about features within the context of an installation session. + /// + internal sealed class FeatureInfoCollection : ICollection + { + private Session session; + + internal FeatureInfoCollection(Session session) + { + this.session = session; + } + + /// + /// Gets information about a feature within the context of an installation session. + /// + /// name of the feature + /// feature object + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public FeatureInfo this[string feature] + { + get + { + return new FeatureInfo(this.session, feature); + } + } + + void ICollection.Add(FeatureInfo item) + { + throw new InvalidOperationException(); + } + + void ICollection.Clear() + { + throw new InvalidOperationException(); + } + + /// + /// Checks if the collection contains a feature. + /// + /// name of the feature + /// true if the feature is in the collection, else false + public bool Contains(string feature) + { + return this.session.Database.CountRows( + "Feature", "`Feature` = '" + feature + "'") == 1; + } + + bool ICollection.Contains(FeatureInfo item) + { + return item != null && this.Contains(item.Name); + } + + /// + /// Copies the features into an array. + /// + /// array that receives the features + /// offset into the array + public void CopyTo(FeatureInfo[] array, int arrayIndex) + { + if (array == null) { + throw new ArgumentNullException("array"); + } + foreach (FeatureInfo feature in this) + { + array[arrayIndex++] = feature; + } + } + + /// + /// Gets the number of features defined for the product. + /// + public int Count + { + get + { + return this.session.Database.CountRows("Feature"); + } + } + + bool ICollection.IsReadOnly + { + get + { + return true; + } + } + + bool ICollection.Remove(FeatureInfo item) + { + throw new InvalidOperationException(); + } + + /// + /// Enumerates the features in the collection. + /// + /// an enumerator over all features in the collection + public IEnumerator GetEnumerator() + { + using (View featureView = this.session.Database.OpenView( + "SELECT `Feature` FROM `Feature`")) + { + featureView.Execute(); + + foreach (Record featureRec in featureView) using (featureRec) + { + string feature = featureRec.GetString(1); + yield return new FeatureInfo(this.session, feature); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + } + + /// + /// Provides access to information about a feature within the context of an installation session. + /// + internal class FeatureInfo + { + private Session session; + private string name; + + internal FeatureInfo(Session session, string name) + { + this.session = session; + this.name = name; + } + + /// + /// Gets the name of the feature (primary key in the Feature table). + /// + public string Name + { + get + { + return this.name; + } + } + + /// + /// Gets the current install state of the feature. + /// + /// the Session handle is invalid + /// an unknown feature was requested + ///

+ /// Win32 MSI API: + /// MsiGetFeatureState + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public InstallState CurrentState + { + get + { + int installState, actionState; + uint ret = RemotableNativeMethods.MsiGetFeatureState((int) this.session.Handle, this.name, out installState, out actionState); + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.UNKNOWN_FEATURE) + { + throw InstallerException.ExceptionFromReturnCode(ret, this.name); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + if (installState == (int) InstallState.Advertised) + { + return InstallState.Advertised; + } + return (InstallState) installState; + } + } + + /// + /// Gets or sets the action state of the feature. + /// + /// the Session handle is invalid + /// an unknown feature was requested + ///

+ /// When changing the feature action, the action state of all the Components linked to the changed + /// Feature records are also updated appropriately, based on the new feature Select state. + /// All Features can be configured at once by specifying the keyword ALL instead of a specific feature name. + ///

+ /// Win32 MSI APIs: + /// MsiGetFeatureState, + /// MsiSetFeatureState + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public InstallState RequestState + { + get + { + int installState, actionState; + uint ret = RemotableNativeMethods.MsiGetFeatureState((int) this.session.Handle, this.name, out installState, out actionState); + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.UNKNOWN_FEATURE) + { + throw InstallerException.ExceptionFromReturnCode(ret, this.name); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + return (InstallState) actionState; + } + + set + { + uint ret = RemotableNativeMethods.MsiSetFeatureState((int) this.session.Handle, this.name, (int) value); + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.UNKNOWN_FEATURE) + { + throw InstallerException.ExceptionFromReturnCode(ret, this.name); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + } + } + + /// + /// Gets a list of valid installation states for the feature. + /// + /// the Session handle is invalid + /// an unknown feature was requested + ///

+ /// Win32 MSI API: + /// MsiGetFeatureValidStates + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public ICollection ValidStates + { + get + { + List states = new List(); + uint installState; + uint ret = RemotableNativeMethods.MsiGetFeatureValidStates((int) this.session.Handle, this.name, out installState); + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.UNKNOWN_FEATURE) + { + throw InstallerException.ExceptionFromReturnCode(ret, this.name); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + for (int i = 1; i <= (int) InstallState.Default; i++) + { + if (((int) installState & (1 << i)) != 0) + { + states.Add((InstallState) i); + } + } + return states.AsReadOnly(); + } + } + + /// + /// Gets or sets the attributes of the feature. + /// + /// the Session handle is invalid + /// an unknown feature was requested + ///

+ /// Win32 MSI APIs: + /// MsiGetFeatureInfo, + /// MsiSetFeatureAttributes + ///

+ /// Since the lpAttributes paramter of + /// MsiGetFeatureInfo + /// does not contain an equivalent flag for , this flag will + /// not be retrieved. + ///

+ /// Since the dwAttributes parameter of + /// MsiSetFeatureAttributes + /// does not contain an equivalent flag for , the presence + /// of this flag will be ignored. + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public FeatureAttributes Attributes + { + get + { + FeatureAttributes attributes; + uint titleBufSize = 0; + uint descBufSize = 0; + uint attr; + uint ret = NativeMethods.MsiGetFeatureInfo( + (int) this.session.Handle, + this.name, + out attr, + null, + ref titleBufSize, + null, + ref descBufSize); + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + // Values for attributes that MsiGetFeatureInfo returns are + // double the values in the Attributes column of the Feature Table. + attributes = (FeatureAttributes) (attr >> 1); + + // MsiGetFeatureInfo MSDN documentation indicates + // NOUNSUPPORTEDADVERTISE is 32. Conversion above changes this to 16 + // which is UIDisallowAbsent. MsiGetFeatureInfo isn't documented to + // return an attribute for 'UIDisallowAbsent', so if UIDisallowAbsent + // is set, change it to NoUnsupportedAdvertise which then maps correctly + // to NOUNSUPPORTEDADVERTISE. + if ((attributes & FeatureAttributes.UIDisallowAbsent) == FeatureAttributes.UIDisallowAbsent) + { + attributes &= ~FeatureAttributes.UIDisallowAbsent; + attributes |= FeatureAttributes.NoUnsupportedAdvertise; + } + + return attributes; + } + + set + { + // MsiSetFeatureAttributes doesn't indicate UIDisallowAbsent is valid + // so remove it. + FeatureAttributes attributes = value; + attributes &= ~FeatureAttributes.UIDisallowAbsent; + + // Values for attributes that MsiSetFeatureAttributes uses are + // double the values in the Attributes column of the Feature Table. + uint attr = ((uint) attributes) << 1; + + // MsiSetFeatureAttributes MSDN documentation indicates + // NOUNSUPPORTEDADVERTISE is 32. Conversion above changes this to 64 + // which is undefined. Change this back to 32. + uint noUnsupportedAdvertiseDbl = ((uint)FeatureAttributes.NoUnsupportedAdvertise) << 1; + if ((attr & noUnsupportedAdvertiseDbl) == noUnsupportedAdvertiseDbl) + { + attr &= ~noUnsupportedAdvertiseDbl; + attr |= (uint) FeatureAttributes.NoUnsupportedAdvertise; + } + + uint ret = RemotableNativeMethods.MsiSetFeatureAttributes((int) this.session.Handle, this.name, attr); + + if (ret != (uint)NativeMethods.Error.SUCCESS) + { + if (ret == (uint)NativeMethods.Error.UNKNOWN_FEATURE) + { + throw InstallerException.ExceptionFromReturnCode(ret, this.name); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + } + } + + /// + /// Gets the title of the feature. + /// + /// the Session handle is invalid + /// an unknown feature was requested + ///

+ /// Win32 MSI API: + /// MsiGetFeatureInfo + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string Title + { + get + { + StringBuilder titleBuf = new StringBuilder(80); + uint titleBufSize = (uint) titleBuf.Capacity; + uint descBufSize = 0; + uint attr; + uint ret = NativeMethods.MsiGetFeatureInfo( + (int) this.session.Handle, + this.name, + out attr, + titleBuf, + ref titleBufSize, + null, + ref descBufSize); + + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + titleBuf.Capacity = (int) ++titleBufSize; + ret = NativeMethods.MsiGetFeatureInfo( + (int) this.session.Handle, + this.name, + out attr, + titleBuf, + ref titleBufSize, + null, + ref descBufSize); + } + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + return titleBuf.ToString(); + } + } + + /// + /// Gets the description of the feature. + /// + /// the Session handle is invalid + /// an unknown feature was requested + ///

+ /// Win32 MSI API: + /// MsiGetFeatureInfo + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string Description + { + get + { + StringBuilder descBuf = new StringBuilder(256); + uint titleBufSize = 0; + uint descBufSize = (uint) descBuf.Capacity; + uint attr; + uint ret = NativeMethods.MsiGetFeatureInfo( + (int) this.session.Handle, + this.name, + out attr, + null, + ref titleBufSize, + descBuf, + ref descBufSize); + + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + descBuf.Capacity = (int) ++descBufSize; + ret = NativeMethods.MsiGetFeatureInfo( + (int) this.session.Handle, + this.name, + out attr, + null, + ref titleBufSize, + descBuf, + ref descBufSize); + } + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + return descBuf.ToString(); + } + } + + /// + /// Calculates the disk space required by the feature and its selected children and parent features. + /// + /// If true, the parent features are included in the cost. + /// If true, the child features are included in the cost. + /// Specifies the installation state. + /// The disk space requirement in bytes. + ///

+ /// Win32 MSI API: + /// MsiGetFeatureCost + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public long GetCost(bool includeParents, bool includeChildren, InstallState installState) + { + const int MSICOSTTREE_CHILDREN = 1; + const int MSICOSTTREE_PARENTS = 2; + + int cost; + uint ret = RemotableNativeMethods.MsiGetFeatureCost( + (int) this.session.Handle, + this.name, + (includeParents ? MSICOSTTREE_PARENTS : 0) | (includeChildren ? MSICOSTTREE_CHILDREN : 0), + (int) installState, + out cost); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return cost * 512L; + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/FeatureInstallation.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/FeatureInstallation.cs new file mode 100644 index 000000000..c19316505 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/FeatureInstallation.cs @@ -0,0 +1,186 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Text; + + /// + /// Represents an instance of a feature of an installed product. + /// + internal class FeatureInstallation : InstallationPart + { + /// + /// Creates a new FeatureInstallation instance for a feature of a product. + /// + /// feature name + /// ProductCode GUID + public FeatureInstallation(string featureName, string productCode) + : base(featureName, productCode) + { + if (string.IsNullOrWhiteSpace(featureName)) + { + throw new ArgumentNullException("featureName"); + } + } + + /// + /// Gets the name of the feature. + /// + public string FeatureName + { + get + { + return this.Id; + } + } + + /// + /// Gets the installed state of the feature. + /// + ///

+ /// Win32 MSI API: + /// MsiQueryFeatureState + ///

+ public override InstallState State + { + get + { + int installState = NativeMethods.MsiQueryFeatureState( + this.ProductCode, this.FeatureName); + return (InstallState) installState; + } + } + + /// + /// Gets the parent of the feature, or null if the feature has no parent (it is a root feature). + /// + /// + /// Invocation of this property may be slightly costly for products with many features, + /// because it involves an enumeration of all the features in the product. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public FeatureInstallation Parent + { + get + { + StringBuilder featureBuf = new StringBuilder(256); + StringBuilder parentBuf = new StringBuilder(256); + for (uint i = 0; ; i++) + { + uint ret = NativeMethods.MsiEnumFeatures(this.ProductCode, i, featureBuf, parentBuf); + + if (ret != 0) + { + break; + } + + if (featureBuf.ToString() == this.FeatureName) + { + if (parentBuf.Length > 0) + { + return new FeatureInstallation(parentBuf.ToString(), this.ProductCode); + } + else + { + return null; + } + } + } + + return null; + } + } + + /// + /// Gets the usage metrics for the feature. + /// + ///

+ /// If no usage metrics are recorded, the value is 0. + ///

+ /// Win32 MSI API: + /// MsiGetFeatureUsage + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public FeatureInstallation.UsageData Usage + { + get + { + uint useCount; + ushort useDate; + uint ret = NativeMethods.MsiGetFeatureUsage( + this.ProductCode, this.FeatureName, out useCount, out useDate); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + DateTime lastUsedDate; + if (useCount == 0) + { + lastUsedDate = DateTime.MinValue; + } + else + { + lastUsedDate = new DateTime( + 1980 + (useDate >> 9), + (useDate & 0x01FF) >> 5, + (useDate & 0x001F)); + } + + return new UsageData((int) useCount, lastUsedDate); + } + } + + /// + /// Holds data about the usage of a feature. + /// + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible")] + [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes")] + public struct UsageData + { + private int useCount; + private DateTime lastUsedDate; + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UsageData(int useCount, DateTime lastUsedDate) + { + this.useCount = useCount; + this.lastUsedDate = lastUsedDate; + } + + /// + /// Gets count of the number of times the feature has been used. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public int UseCount + { + get + { + return this.useCount; + } + } + + /// + /// Gets the date the feature was last used. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public DateTime LastUsedDate + { + get + { + return this.lastUsedDate; + } + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Handle.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Handle.cs new file mode 100644 index 000000000..317c0ac6b --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Handle.cs @@ -0,0 +1,160 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Diagnostics.CodeAnalysis; + + /// + /// Base class for Windows Installer handle types (Database, View, Record, SummaryInfo). + /// + ///

+ /// These classes implement the interface, because they + /// hold unmanaged resources (MSI handles) that should be properly disposed + /// when no longer needed. + ///

+ public abstract class InstallerHandle : MarshalByRefObject, IDisposable + { + private NativeMethods.MsiHandle handle; + + /// + /// Constructs a handle object from a native integer handle. + /// + /// Native integer handle. + /// true to close the handle when this object is disposed or finalized + protected InstallerHandle(IntPtr handle, bool ownsHandle) + { + if (handle == IntPtr.Zero) + { + throw new InvalidHandleException(); + } + + this.handle = new NativeMethods.MsiHandle(handle, ownsHandle); + } + + /// + /// Gets the native integer handle. + /// + [SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")] + public IntPtr Handle + { + get + { + if (this.IsClosed) + { + throw new InvalidHandleException(); + } + return this.handle; + } + } + + /// + /// Checks if the handle is closed. When closed, method calls on the handle object may throw an . + /// + [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")] + public bool IsClosed + { + get + { + return this.handle.IsClosed; + } + } + + /// + /// Closes the handle. After closing a handle, further method calls may throw an . + /// + ///

+ /// The finalizer of this class will NOT close the handle if it is still open, + /// because finalization can run on a separate thread from the application, + /// resulting in potential problems if handles are closed from that thread. + /// It is best that the handle be closed manually as soon as it is no longer needed, + /// as leaving lots of unused handles open can degrade performance. + ///

+ /// Win32 MSI API: + /// MsiCloseHandle + ///

+ /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Closes the handle. After closing a handle, further method calls may throw an . + /// + ///

+ /// The finalizer of this class will NOT close the handle if it is still open, + /// because finalization can run on a separate thread from the application, + /// resulting in potential problems if handles are closed from that thread. + /// It is best that the handle be closed manually as soon as it is no longer needed, + /// as leaving lots of unused handles open can degrade performance. + ///

+ /// This method is merely an alias for the method. + ///

+ /// Win32 MSI API: + /// MsiCloseHandle + ///

+ public void Close() + { + this.Dispose(); + } + + /// + /// Tests whether this handle object is equal to another handle object. Two handle objects are equal + /// if their types are the same and their native integer handles are the same. + /// + /// The handle object to compare with the current handle object. + /// true if the specified handle object is equal to the current handle object; otherwise false + public override bool Equals(object obj) + { + return (obj != null && this.GetType() == obj.GetType() && + this.Handle == ((InstallerHandle) obj).Handle); + } + + /// + /// Gets a hash value for the handle object. + /// + /// A hash code for the handle object. + ///

+ /// The hash code is derived from the native integer handle. + ///

+ public override int GetHashCode() + { + return this.Handle.GetHashCode(); + } + + /// + /// Gets an object that can be used internally for safe syncronization. + /// + internal object Sync + { + get + { + return this.handle; + } + } + + /// + /// Closes the handle. After closing a handle, further method calls may throw an . + /// + /// If true, the method has been called directly or indirectly by a user's code, + /// so managed and unmanaged resources will be disposed. If false, the method has been called by the + /// runtime from inside the finalizer, and only unmanaged resources will be disposed. + [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")] + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + this.handle.Dispose(); + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/IEmbeddedUI.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/IEmbeddedUI.cs new file mode 100644 index 000000000..242b129a9 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/IEmbeddedUI.cs @@ -0,0 +1,74 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System.Diagnostics.CodeAnalysis; + + /// + /// [MSI 4.5] Interface for an embedded external user interface for an installation. + /// + /// + /// Classes which implement this interface must have a public constructor that takes no parameters. + /// + internal interface IEmbeddedUI + { + /// + /// Initializes the embedded UI. + /// + /// Handle to the installer which can be used to get and set properties. + /// The handle is only valid for the duration of this method call. + /// Path to the directory that contains all the files from the MsiEmbeddedUI table. + /// On entry, contains the current UI level for the installation. After this + /// method returns, the installer resets the UI level to the returned value of this parameter. + /// True if the embedded UI was successfully initialized; false if the installation + /// should continue without the embedded UI. + /// The installation was canceled by the user. + /// The embedded UI failed to initialize and + /// causes the installation to fail. + ///

+ /// Win32 MSI API: + /// InitializeEmbeddedUI + ///

+ [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference")] + bool Initialize(Session session, string resourcePath, ref InstallUIOptions internalUILevel); + + /// + /// Processes information and progress messages sent to the user interface. + /// + /// Message type. + /// Record that contains message data. + /// Message buttons. + /// Message box icon. + /// Message box default button. + /// Result of processing the message. + ///

+ /// Win32 MSI API: + /// EmbeddedUIHandler + ///

+ MessageResult ProcessMessage( + InstallMessage messageType, + Record messageRecord, + MessageButtons buttons, + MessageIcon icon, + MessageDefaultButton defaultButton); + + /// + /// Shuts down the embedded UI at the end of the installation. + /// + /// + /// If the installation was canceled during initialization, this method will not be called. + /// If the installation was canceled or failed at any later point, this method will be called at the end. + ///

+ /// Win32 MSI API: + /// ShutdownEmbeddedUI + ///

+ void Shutdown(); + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/InstallCost.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/InstallCost.cs new file mode 100644 index 000000000..9f42bf3f0 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/InstallCost.cs @@ -0,0 +1,74 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Microsoft.Deployment.WindowsInstaller.InstallCost struct. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System.Diagnostics.CodeAnalysis; + + /// + /// Represents a per-drive disk space cost for an installation. + /// + [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes")] + public struct InstallCost + { + private string driveName; + private long cost; + private long tempCost; + + /// + /// Creates a new InstallCost object. + /// + /// name of the drive this cost data applies to + /// installation cost on this drive, as a number of bytes + /// temporary disk space required on this drive, as a number of bytes + internal InstallCost(string driveName, long cost, long tempCost) + { + this.driveName = driveName; + this.cost = cost; + this.tempCost = tempCost; + } + + /// + /// The name of the drive this cost data applies to. + /// + public string DriveName + { + get + { + return this.driveName; + } + } + + /// + /// The installation cost on this drive, as a number of bytes. + /// + public long Cost + { + get + { + return this.cost; + } + } + + /// + /// The temporary disk space required on this drive, as a number of bytes. + /// + ///

+ /// This temporary space requirement is space needed only for the duration + /// of the installation, over the final footprint on disk. + ///

+ public long TempCost + { + get + { + return this.tempCost; + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Installation.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Installation.cs new file mode 100644 index 000000000..d82a3ab2b --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Installation.cs @@ -0,0 +1,103 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + /// + /// Subclasses of this abstract class represent a unique instance of a + /// registered product or patch installation. + /// + public abstract class Installation + { + private string installationCode; + private string userSid; + private UserContexts context; + private SourceList sourceList; + + internal Installation(string installationCode, string userSid, UserContexts context) + { + if (context == UserContexts.Machine) + { + userSid = null; + } + this.installationCode = installationCode; + this.userSid = userSid; + this.context = context; + } + + /// + /// Gets the user security identifier (SID) under which this product or patch + /// installation is available. + /// + public string UserSid + { + get + { + return this.userSid; + } + } + + /// + /// Gets the user context of this product or patch installation. + /// + public UserContexts Context + { + get + { + return this.context; + } + } + + /// + /// Gets the source list of this product or patch installation. + /// + internal virtual SourceList SourceList + { + get + { + if (this.sourceList == null) + { + this.sourceList = new SourceList(this); + } + return this.sourceList; + } + } + + /// + /// Gets a value indicating whether this product or patch is installed on the current system. + /// + public abstract bool IsInstalled + { + get; + } + + internal string InstallationCode + { + get + { + return this.installationCode; + } + } + + internal abstract int InstallationType + { + get; + } + + /// + /// Gets a property about the product or patch installation. + /// + /// Name of the property being retrieved. + /// + public abstract string this[string propertyName] + { + get; + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/InstallationPart.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/InstallationPart.cs new file mode 100644 index 000000000..9f855dc8f --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/InstallationPart.cs @@ -0,0 +1,90 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + /// + /// Subclasses of this abstract class represent an instance + /// of a registered feature or component. + /// + internal abstract class InstallationPart + { + private string id; + private string productCode; + private string userSid; + private UserContexts context; + + internal InstallationPart(string id, string productCode) + : this(id, productCode, null, UserContexts.None) + { + } + + internal InstallationPart(string id, string productCode, string userSid, UserContexts context) + { + this.id = id; + this.productCode = productCode; + this.userSid = userSid; + this.context = context; + } + + internal string Id + { + get + { + return this.id; + } + } + + internal string ProductCode + { + get + { + return this.productCode; + } + } + + internal string UserSid + { + get + { + return this.userSid; + } + } + + internal UserContexts Context + { + get + { + return this.context; + } + } + + /// + /// Gets the product that this item is a part of. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ProductInstallation Product + { + get + { + return this.productCode != null ? + new ProductInstallation(this.productCode, userSid, context) : null; + } + } + + /// + /// Gets the current installation state of the item. + /// + public abstract InstallState State + { + get; + } + } + +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Installer.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Installer.cs new file mode 100644 index 000000000..02de57ca5 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Installer.cs @@ -0,0 +1,895 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Resources; + using System.Text; + + /// +/// Receives an exception from +/// +/// indicating the reason a particular patch is not applicable to a product. +/// +/// MSP file path, XML file path, or XML blob that was passed to +/// +/// exception indicating the reason the patch is not applicable +///

+/// If is an or subclass, then +/// its and +/// properties will indicate a more specific reason the patch was not applicable. +///

+/// The could also be a FileNotFoundException if the +/// patch string was a file path. +///

+public delegate void InapplicablePatchHandler(string patch, Exception exception); + +/// +/// Provides static methods for installing and configuring products and patches. +/// +internal static partial class Installer +{ + private static bool rebootRequired; + private static bool rebootInitiated; + private static ResourceManager errorResources; + + /// + /// Indicates whether a system reboot is required after running an installation or configuration operation. + /// + public static bool RebootRequired + { + get + { + return Installer.rebootRequired; + } + } + + /// + /// Indicates whether a system reboot has been initiated after running an installation or configuration operation. + /// + public static bool RebootInitiated + { + get + { + return Installer.rebootInitiated; + } + } + + /// + /// Enables the installer's internal user interface. Then this user interface is used + /// for all subsequent calls to user-interface-generating installer functions in this process. + /// + /// Specifies the level of complexity of the user interface + /// Handle to a window, which becomes the owner of any user interface created. + /// A pointer to the previous owner of the user interface is returned. + /// The previous user interface level + ///

+ /// Win32 MSI API: + /// MsiSetInternalUI + ///

+ [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference")] + public static InstallUIOptions SetInternalUI(InstallUIOptions uiOptions, ref IntPtr windowHandle) + { + return (InstallUIOptions) NativeMethods.MsiSetInternalUI((uint) uiOptions, ref windowHandle); + } + + /// + /// Enables the installer's internal user interface. Then this user interface is used + /// for all subsequent calls to user-interface-generating installer functions in this process. + /// The owner of the user interface does not change. + /// + /// Specifies the level of complexity of the user interface + /// The previous user interface level + ///

+ /// Win32 MSI API: + /// MsiSetInternalUI + ///

+ public static InstallUIOptions SetInternalUI(InstallUIOptions uiOptions) + { + return (InstallUIOptions) NativeMethods.MsiSetInternalUI((uint) uiOptions, IntPtr.Zero); + } + + /// + /// Enables logging of the selected message type for all subsequent install sessions in + /// the current process space. + /// + /// One or more mode flags specifying the type of messages to log + /// Full path to the log file. A null path disables logging, + /// in which case the logModes paraneter is ignored. + /// an invalid log mode was specified + /// This method takes effect on any new installation processes. Calling this + /// method from within a custom action will not start logging for that installation. + public static void EnableLog(InstallLogModes logModes, string logFile) + { + Installer.EnableLog(logModes, logFile, false, true); + } + + /// + /// Enables logging of the selected message type for all subsequent install sessions in + /// the current process space. + /// + /// One or more mode flags specifying the type of messages to log + /// Full path to the log file. A null path disables logging, + /// in which case the logModes paraneter is ignored. + /// If true, the log lines will be appended to any existing file content. + /// If false, the log file will be truncated if it exists. The default is false. + /// If true, the log will be flushed after every line. + /// If false, the log will be flushed every 20 lines. The default is true. + /// an invalid log mode was specified + ///

+ /// This method takes effect on any new installation processes. Calling this + /// method from within a custom action will not start logging for that installation. + ///

+ /// Win32 MSI API: + /// MsiEnableLog + ///

+ public static void EnableLog(InstallLogModes logModes, string logFile, bool append, bool flushEveryLine) + { + uint ret = NativeMethods.MsiEnableLog((uint) logModes, logFile, (append ? (uint) 1 : 0) + (flushEveryLine ? (uint) 2 : 0)); + if (ret != 0 && ret != (uint) NativeMethods.Error.FILE_INVALID) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// increments the usage count for a particular feature and returns the installation state for + /// that feature. This method should be used to indicate an application's intent to use a feature. + /// + /// The product code of the product. + /// The feature to be used. + /// Must have the value . + /// The installed state of the feature. + ///

+ /// The UseFeature method should only be used on features known to be published. The application + /// should determine the status of the feature by calling either the FeatureState method or + /// Features method. + ///

+ /// Win32 MSI APIs: + /// MsiUseFeature, + /// MsiUseFeatureEx + ///

+ public static InstallState UseFeature(string productCode, string feature, InstallMode installMode) + { + int installState = NativeMethods.MsiUseFeatureEx(productCode, feature, unchecked ((uint) installMode), 0); + return (InstallState) installState; + } + + /// + /// Opens an installer package for use with functions that access the product database and install engine, + /// returning an Session object. + /// + /// Path to the package + /// Specifies whether or not the create a Session object that ignores the + /// computer state and that is incapable of changing the current computer state. A value of false yields + /// the normal behavior. A value of true creates a "safe" Session object that cannot change of the current + /// machine state. + /// A Session object allowing access to the product database and install engine + /// The product could not be opened + /// The installer configuration data is corrupt + ///

+ /// Note that only one Session object can be opened by a single process. OpenPackage cannot be used in a + /// custom action because the active installation is the only session allowed. + ///

+ /// A "safe" Session object ignores the current computer state when opening the package and prevents + /// changes to the current computer state. + ///

+ /// The Session object should be d after use. + /// It is best that the handle be closed manually as soon as it is no longer + /// needed, as leaving lots of unused handles open can degrade performance. + ///

+ /// Win32 MSI APIs: + /// MsiOpenPackage, + /// MsiOpenPackageEx + ///

+ public static Session OpenPackage(string packagePath, bool ignoreMachineState) + { + int sessionHandle; + uint ret = NativeMethods.MsiOpenPackageEx(packagePath, ignoreMachineState ? (uint) 1 : 0, out sessionHandle); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return new Session((IntPtr) sessionHandle, true); + } + + /// + /// Opens an installer package for use with functions that access the product database and install engine, + /// returning an Session object. + /// + /// Database used to create the session + /// Specifies whether or not the create a Session object that ignores the + /// computer state and that is incapable of changing the current computer state. A value of false yields + /// the normal behavior. A value of true creates a "safe" Session object that cannot change of the current + /// machine state. + /// A Session object allowing access to the product database and install engine + /// The product could not be opened + /// The installer configuration data is corrupt + ///

+ /// Note that only one Session object can be opened by a single process. OpenPackage cannot be used in a + /// custom action because the active installation is the only session allowed. + ///

+ /// A "safe" Session object ignores the current computer state when opening the package and prevents + /// changes to the current computer state. + ///

+ /// The Session object should be d after use. + /// It is best that the handle be closed manually as soon as it is no longer + /// needed, as leaving lots of unused handles open can degrade performance. + ///

+ /// Win32 MSI APIs: + /// MsiOpenPackage, + /// MsiOpenPackageEx + ///

+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] + public static Session OpenPackage(Database database, bool ignoreMachineState) + { + if (database == null) + { + throw new ArgumentNullException("database"); + } + + return Installer.OpenPackage( + String.Format(CultureInfo.InvariantCulture, "#{0}", database.Handle), + ignoreMachineState); + } + + /// + /// Opens an installer package for an installed product using the product code. + /// + /// Product code of the installed product + /// A Session object allowing access to the product database and install engine, + /// or null if the specified product is not installed. + /// An unknown product was requested + /// The product could not be opened + /// The installer configuration data is corrupt + ///

+ /// Note that only one Session object can be opened by a single process. OpenProduct cannot be + /// used in a custom action because the active installation is the only session allowed. + ///

+ /// The Session object should be d after use. + /// It is best that the handle be closed manually as soon as it is no longer + /// needed, as leaving lots of unused handles open can degrade performance. + ///

+ /// Win32 MSI API: + /// MsiOpenProduct + ///

+ public static Session OpenProduct(string productCode) + { + int sessionHandle; + uint ret = NativeMethods.MsiOpenProduct(productCode, out sessionHandle); + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.UNKNOWN_PRODUCT) + { + return null; + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + return new Session((IntPtr) sessionHandle, true); + } + + /// + /// Gets the full component path, performing any necessary installation. This method prompts for source if + /// necessary and increments the usage count for the feature. + /// + /// Product code for the product that contains the feature with the necessary component + /// Feature ID of the feature with the necessary component + /// Component code of the necessary component + /// Installation mode; this can also include bits from + /// Path to the component + ///

+ /// Win32 MSI API: + /// MsiProvideComponent + ///

+ public static string ProvideComponent(string product, string feature, string component, InstallMode installMode) + { + StringBuilder pathBuf = new StringBuilder(512); + uint pathBufSize = (uint) pathBuf.Capacity; + uint ret = NativeMethods.MsiProvideComponent(product, feature, component, unchecked((uint)installMode), pathBuf, ref pathBufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + pathBuf.Capacity = (int) ++pathBufSize; + ret = NativeMethods.MsiProvideComponent(product, feature, component, unchecked((uint)installMode), pathBuf, ref pathBufSize); + } + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return pathBuf.ToString(); + } + + /// + /// Gets the full component path for a qualified component that is published by a product and + /// performs any necessary installation. This method prompts for source if necessary and increments + /// the usage count for the feature. + /// + /// Specifies the component ID for the requested component. This may not be the + /// GUID for the component itself but rather a server that provides the correct functionality, as in the + /// ComponentId column of the PublishComponent table. + /// Specifies a qualifier into a list of advertising components (from PublishComponent Table). + /// Installation mode; this can also include bits from + /// Optional; specifies the product to match that has published the qualified component. + /// Path to the component + ///

+ /// Win32 MSI APIs: + /// MsiProvideQualifiedComponent + /// MsiProvideQualifiedComponentEx + ///

+ public static string ProvideQualifiedComponent(string component, string qualifier, InstallMode installMode, string product) + { + StringBuilder pathBuf = new StringBuilder(512); + uint pathBufSize = (uint) pathBuf.Capacity; + uint ret = NativeMethods.MsiProvideQualifiedComponentEx(component, qualifier, unchecked((uint)installMode), product, 0, 0, pathBuf, ref pathBufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + pathBuf.Capacity = (int) ++pathBufSize; + ret = NativeMethods.MsiProvideQualifiedComponentEx(component, qualifier, unchecked((uint)installMode), product, 0, 0, pathBuf, ref pathBufSize); + } + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return pathBuf.ToString(); + } + + /// + /// Gets the full path to a Windows Installer component containing an assembly. This method prompts for a source and + /// increments the usage count for the feature. + /// + /// Assembly name + /// Set to null for global assemblies. For private assemblies, set to the full path of the + /// application configuration file (.cfg file) or executable file (.exe) of the application to which the assembly + /// has been made private. + /// Installation mode; this can also include bits from + /// True if this is a Win32 assembly, false if it is a .NET assembly + /// Path to the assembly + ///

+ /// Win32 MSI API: + /// MsiProvideAssembly + ///

+ public static string ProvideAssembly(string assemblyName, string appContext, InstallMode installMode, bool isWin32Assembly) + { + StringBuilder pathBuf = new StringBuilder(512); + uint pathBufSize = (uint) pathBuf.Capacity; + uint ret = NativeMethods.MsiProvideAssembly(assemblyName, appContext, unchecked ((uint) installMode), (isWin32Assembly ? (uint) 1 : 0), pathBuf, ref pathBufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + pathBuf.Capacity = (int) ++pathBufSize; + ret = NativeMethods.MsiProvideAssembly(assemblyName, appContext, unchecked ((uint) installMode), (isWin32Assembly ? (uint) 1 : 0), pathBuf, ref pathBufSize); + } + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return pathBuf.ToString(); + } + + /// + /// Installs files that are unexpectedly missing. + /// + /// Product code for the product that owns the component to be installed + /// Component to be installed + /// Specifies the way the component should be installed. + /// the user exited the installation + ///

+ /// Win32 MSI API: + /// MsiInstallMissingComponent + ///

+ public static void InstallMissingComponent(string product, string component, InstallState installState) + { + uint ret = NativeMethods.MsiInstallMissingComponent(product, component, (int) installState); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Installs files that are unexpectedly missing. + /// + /// Product code for the product that owns the file to be installed + /// File to be installed + /// the user exited the installation + ///

+ /// Win32 MSI API: + /// MsiInstallMissingFile + ///

+ public static void InstallMissingFile(string product, string file) + { + uint ret = NativeMethods.MsiInstallMissingFile(product, file); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Reinstalls a feature. + /// + /// Product code for the product containing the feature to be reinstalled + /// Feature to be reinstalled + /// Reinstall modes + /// the user exited the installation + ///

+ /// Win32 MSI API: + /// MsiReinstallFeature + ///

+ public static void ReinstallFeature(string product, string feature, ReinstallModes reinstallModes) + { + uint ret = NativeMethods.MsiReinstallFeature(product, feature, (uint) reinstallModes); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Reinstalls a product. + /// + /// Product code for the product to be reinstalled + /// Reinstall modes + /// the user exited the installation + ///

+ /// Win32 MSI API: + /// MsiReinstallProduct + ///

+ public static void ReinstallProduct(string product, ReinstallModes reinstallModes) + { + uint ret = NativeMethods.MsiReinstallProduct(product, (uint) reinstallModes); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Opens an installer package and initializes an install session. + /// + /// path to the patch package + /// command line property settings + /// There was an error installing the product + ///

+ /// To completely remove a product, set REMOVE=ALL in . + ///

+ /// This method displays the user interface with the current settings and + /// log mode. You can change user interface settings with the + /// and functions. You can set the log mode with the + /// function. + ///

+ /// The and properties should be + /// tested after calling this method. + ///

+ /// Win32 MSI API: + /// MsiInstallProduct + ///

+ public static void InstallProduct(string packagePath, string commandLine) + { + uint ret = NativeMethods.MsiInstallProduct(packagePath, commandLine); + Installer.CheckInstallResult(ret); + } + + /// + /// Installs or uninstalls a product. + /// + /// Product code of the product to be configured. + /// Specifies the default installation configuration of the + /// product. The parameter is ignored and all features + /// are installed if the parameter is set to any other + /// value than . This parameter must be either 0 + /// (install using authored feature levels), 65535 (install all features), or a value + /// between 0 and 65535 to install a subset of available features. + /// Specifies the installation state for the product. + /// Specifies the command line property settings. This should + /// be a list of the format Property=Setting Property=Setting. + /// There was an error configuring the product + ///

+ /// This method displays the user interface with the current settings and + /// log mode. You can change user interface settings with the + /// and functions. You can set the log mode with the + /// function. + ///

+ /// The and properties should be + /// tested after calling this method. + ///

+ /// Win32 MSI APIs: + /// MsiConfigureProduct, + /// MsiConfigureProductEx + ///

+ public static void ConfigureProduct(string productCode, int installLevel, InstallState installState, string commandLine) + { + uint ret = NativeMethods.MsiConfigureProductEx(productCode, installLevel, (int) installState, commandLine); + Installer.CheckInstallResult(ret); + } + + /// + /// Configures the installed state for a product feature. + /// + /// Product code of the product to be configured. + /// Specifies the feature ID for the feature to be configured. + /// Specifies the installation state for the feature. + /// There was an error configuring the feature + ///

+ /// The and properties should be + /// tested after calling this method. + ///

+ /// Win32 MSI API: + /// MsiConfigureFeature + ///

+ public static void ConfigureFeature(string productCode, string feature, InstallState installState) + { + uint ret = NativeMethods.MsiConfigureFeature(productCode, feature, (int) installState); + Installer.CheckInstallResult(ret); + } + + /// + /// For each product listed by the patch package as eligible to receive the patch, ApplyPatch invokes + /// an installation and sets the PATCH property to the path of the patch package. + /// + /// path to the patch package + /// optional command line property settings + /// There was an error applying the patch + ///

+ /// The and properties should be + /// tested after calling this method. + ///

+ /// Win32 MSI API: + /// MsiApplyPatch + ///

+ public static void ApplyPatch(string patchPackage, string commandLine) + { + Installer.ApplyPatch(patchPackage, null, InstallType.Default, commandLine); + } + + /// + /// For each product listed by the patch package as eligible to receive the patch, ApplyPatch invokes + /// an installation and sets the PATCH property to the path of the patch package. + /// + /// path to the patch package + /// path to the product to be patched, if installType + /// is set to + /// type of installation to patch + /// optional command line property settings + /// There was an error applying the patch + ///

+ /// The and properties should be + /// tested after calling this method. + ///

+ /// Win32 MSI API: + /// MsiApplyPatch + ///

+ public static void ApplyPatch(string patchPackage, string installPackage, InstallType installType, string commandLine) + { + uint ret = NativeMethods.MsiApplyPatch(patchPackage, installPackage, (int) installType, commandLine); + Installer.CheckInstallResult(ret); + } + + /// + /// Removes one or more patches from a single product. To remove a patch from + /// multiple products, RemovePatches must be called for each product. + /// + /// List of patches to remove. Each patch can be specified by the GUID + /// of the patch or the full path to the patch package. + /// The ProductCode (GUID) of the product from which the patches + /// are removed. This parameter cannot be null. + /// optional command line property settings + /// There was an error removing the patches + ///

+ /// The and properties should be + /// tested after calling this method. + ///

+ /// Win32 MSI API: + /// MsiRemovePatches + ///

+ public static void RemovePatches(IList patches, string productCode, string commandLine) + { + if (patches == null || patches.Count == 0) + { + throw new ArgumentNullException("patches"); + } + + if (productCode == null) + { + throw new ArgumentNullException("productCode"); + } + + StringBuilder patchList = new StringBuilder(); + foreach (string patch in patches) + { + if (patch != null) + { + if (patchList.Length != 0) + { + patchList.Append(';'); + } + + patchList.Append(patch); + } + } + + if (patchList.Length == 0) + { + throw new ArgumentNullException("patches"); + } + + uint ret = NativeMethods.MsiRemovePatches(patchList.ToString(), productCode, (int) InstallType.SingleInstance, commandLine); + Installer.CheckInstallResult(ret); + } + + /// + /// Determines which patches apply to a specified product MSI and in what sequence. + /// + /// Full path to an MSI file that is the target product + /// for the set of patches. + /// An array of strings specifying the patches to be checked. Each item + /// may be the path to an MSP file, the path an XML file, or just an XML blob. + /// Callback to be invoked for each inapplicable patch, reporting the + /// reason the patch is not applicable. This value may be left null if that information is not + /// desired. + /// An array of selected patch strings from , indicating + /// the set of applicable patches. The items are re-ordered to be in the best sequence. + ///

+ /// If an item in is a file path but does not end in .MSP or .XML, + /// it is assumed to be an MSP file. + ///

+ /// As this overload uses InstallContext.None, it does not consider the current state of + /// the system. + ///

+ /// Win32 MSI API: + /// MsiDetermineApplicablePatches + ///

+ public static IList DetermineApplicablePatches( + string productPackage, + string[] patches, + InapplicablePatchHandler errorHandler) + { + return DetermineApplicablePatches(productPackage, patches, errorHandler, null, UserContexts.None); + } + + /// + /// Determines which patches apply to a specified product and in what sequence. If + /// the product is installed, this method accounts for patches that have already been applied to + /// the product and accounts for obsolete and superceded patches. + /// + /// The product that is the target for the set of patches. This may be + /// either a ProductCode (GUID) of a product that is currently installed, or the path to a an + /// MSI file. + /// An array of strings specifying the patches to be checked. Each item + /// may be the path to an MSP file, the path an XML file, or just an XML blob. + /// Callback to be invoked for each inapplicable patch, reporting the + /// reason the patch is not applicable. This value may be left null if that information is not + /// desired. + /// Specifies a security identifier (SID) of a user. This parameter restricts + /// the context of enumeration for this user account. This parameter cannot be the special SID + /// strings s-1-1-0 (everyone) or s-1-5-18 (local system). If is set to + /// or , then + /// must be null. For the current user context, + /// can be null and can be set to + /// or . + /// Restricts the enumeration to per-user-unmanaged, per-user-managed, + /// or per-machine context, or (if referring to an MSI) to no system context at all. This + /// parameter can be , , + /// , or . + /// An array of selected patch strings from , indicating + /// the set of applicable patches. The items are re-ordered to be in the best sequence. + ///

+ /// If an item in is a file path but does not end in .MSP or .XML, + /// it is assumed to be an MSP file. + ///

+ /// Passing an InstallContext of None only analyzes the MSI file; it does not consider the + /// current state of the system. You cannot use InstallContext.None with a ProductCode GUID. + ///

+ /// Win32 MSI APIs: + /// MsiDetermineApplicablePatches + /// MsiDeterminePatchSequence + ///

+ public static IList DetermineApplicablePatches( + string product, + string[] patches, + InapplicablePatchHandler errorHandler, + string userSid, + UserContexts context) + { + if (string.IsNullOrWhiteSpace(product)) + { + throw new ArgumentNullException("product"); + } + + if (patches == null) + { + throw new ArgumentNullException("patches"); + } + + NativeMethods.MsiPatchSequenceData[] sequenceData = new NativeMethods.MsiPatchSequenceData[patches.Length]; + for (int i = 0; i < patches.Length; i++) + { + if (string.IsNullOrWhiteSpace(patches[i])) + { + throw new ArgumentNullException("patches[" + i + "]"); + } + + sequenceData[i].szPatchData = patches[i]; + sequenceData[i].ePatchDataType = GetPatchStringDataType(patches[i]); + sequenceData[i].dwOrder = -1; + sequenceData[i].dwStatus = 0; + } + + uint ret; + if (context == UserContexts.None) + { + ret = NativeMethods.MsiDetermineApplicablePatches(product, (uint) sequenceData.Length, sequenceData); + } + else + { + ret = NativeMethods.MsiDeterminePatchSequence(product, userSid, context, (uint) sequenceData.Length, sequenceData); + } + + if (errorHandler != null) + { + for (int i = 0; i < sequenceData.Length; i++) + { + if (sequenceData[i].dwOrder < 0 && sequenceData[i].dwStatus != 0) + { + errorHandler(sequenceData[i].szPatchData, InstallerException.ExceptionFromReturnCode(sequenceData[i].dwStatus)); + } + } + } + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + IList patchSeq = new List(patches.Length); + for (int i = 0; i < sequenceData.Length; i++) + { + for (int j = 0; j < sequenceData.Length; j++) + { + if (sequenceData[j].dwOrder == i) + { + patchSeq.Add(sequenceData[j].szPatchData); + } + } + } + return patchSeq; + } + + /// + /// Applies one or more patches to products that are eligible to receive the patch. + /// For each product listed by the patch package as eligible to receive the patch, ApplyPatch invokes + /// an installation and sets the PATCH property to the path of the patch package. + /// + /// The set of patch packages to be applied. + /// Each item is the full path to an MSP file. + /// Provides the ProductCode of the product being patched. If this parameter + /// is null, the patches are applied to all products that are eligible to receive these patches. + /// optional command line property settings + ///

+ /// Win32 MSI API: + /// MsiApplyMultiplePatches + ///

+ public static void ApplyMultiplePatches( + IList patchPackages, string productCode, string commandLine) + { + if (patchPackages == null || patchPackages.Count == 0) + { + throw new ArgumentNullException("patchPackages"); + } + + StringBuilder patchList = new StringBuilder(); + foreach (string patch in patchPackages) + { + if (patch != null) + { + if (patchList.Length != 0) + { + patchList.Append(';'); + } + + patchList.Append(patch); + } + } + + if (patchList.Length == 0) + { + throw new ArgumentNullException("patchPackages"); + } + + uint ret = NativeMethods.MsiApplyMultiplePatches(patchList.ToString(), productCode, commandLine); + Installer.CheckInstallResult(ret); + } + + /// + /// Extracts information from a patch that can be used to determine whether the patch + /// applies on a target system. The method returns an XML string that can be provided to + /// + /// instead of the full patch file. + /// + /// Full path to the patch being queried. + /// XML string containing patch data. + ///

+ /// Win32 MSI API: + /// MsiExtractPatchXMLData + ///

+ public static string ExtractPatchXmlData(string patchPath) + { + StringBuilder buf = new StringBuilder(""); + uint bufSize = 0; + uint ret = NativeMethods.MsiExtractPatchXMLData(patchPath, 0, buf, ref bufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + buf.Capacity = (int) ++bufSize; + ret = NativeMethods.MsiExtractPatchXMLData(patchPath, 0, buf, ref bufSize); + } + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return buf.ToString(); + } + + /// + /// [MSI 3.1] Migrates a user's application configuration data to a new SID. + /// + /// Previous user SID that data is to be migrated from + /// New user SID that data is to be migrated to + ///

+ /// Win32 MSI API: + /// MsiNotifySidChange + ///

+ public static void NotifySidChange(string oldSid, string newSid) + { + uint ret = NativeMethods.MsiNotifySidChange(oldSid, newSid); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + private static void CheckInstallResult(uint ret) + { + switch (ret) + { + case (uint) NativeMethods.Error.SUCCESS: break; + case (uint) NativeMethods.Error.SUCCESS_REBOOT_REQUIRED: Installer.rebootRequired = true; break; + case (uint) NativeMethods.Error.SUCCESS_REBOOT_INITIATED: Installer.rebootInitiated = true; break; + default: throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + private static int GetPatchStringDataType(string patchData) + { + if (patchData.IndexOf("<", StringComparison.Ordinal) >= 0 && + patchData.IndexOf(">", StringComparison.Ordinal) >= 0) + { + return 2; // XML blob + } + else if (String.Compare(Path.GetExtension(patchData), ".xml", + StringComparison.OrdinalIgnoreCase) == 0) + { + return 1; // XML file path + } + else + { + return 0; // MSP file path + } + } +} +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/InstallerAdvertise.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/InstallerAdvertise.cs new file mode 100644 index 000000000..78d1fc708 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/InstallerAdvertise.cs @@ -0,0 +1,278 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Reflection; + using System.Text; + + internal static partial class Installer + { + /// + /// Advertises a product to the local computer. + /// + /// Path to the package of the product being advertised + /// True if the product is user-assigned; false if it is machine-assigned. + /// Semi-colon delimited list of transforms to be applied. This parameter may be null. + /// The language to use if the source supports multiple languages + /// the specified package file does not exist + /// + ///

+ /// Win32 MSI APIs: + /// MsiAdvertiseProduct, + /// MsiAdvertiseProductEx + ///

+ public static void AdvertiseProduct(string packagePath, bool perUser, string transforms, int locale) + { + if (string.IsNullOrWhiteSpace(packagePath)) + { + throw new ArgumentNullException("packagePath"); + } + + if (!File.Exists(packagePath)) + { + throw new FileNotFoundException(null, packagePath); + } + + uint ret = NativeMethods.MsiAdvertiseProduct(packagePath, new IntPtr(perUser ? 1 : 0), transforms, (ushort) locale); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Generates an advertise script. The method enables the installer to write to a + /// script the registry and shortcut information used to assign or publish a product. + /// + /// Path to the package of the product being advertised + /// path to script file to be created with the advertise information + /// Semi-colon delimited list of transforms to be applied. This parameter may be null. + /// The language to use if the source supports multiple languages + /// the specified package file does not exist + /// + ///

+ /// Win32 MSI APIs: + /// MsiAdvertiseProduct, + /// MsiAdvertiseProductEx + ///

+ public static void GenerateAdvertiseScript(string packagePath, string scriptFilePath, string transforms, int locale) + { + if (string.IsNullOrWhiteSpace(packagePath)) + { + throw new ArgumentNullException("packagePath"); + } + + if (!File.Exists(packagePath)) + { + throw new FileNotFoundException(null, packagePath); + } + + uint ret = NativeMethods.MsiAdvertiseProduct(packagePath, scriptFilePath, transforms, (ushort) locale); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Generates an advertise script. The method enables the installer to write to a + /// script the registry and shortcut information used to assign or publish a product. + /// + /// Path to the package of the product being advertised + /// path to script file to be created with the advertise information + /// Semi-colon delimited list of transforms to be applied. This parameter may be null. + /// The language to use if the source supports multiple languages + /// Targeted processor architecture. + /// True to install multiple instances through product code changing transform. + /// Advertises a new instance of the product. Requires that the parameter + /// includes the instance transform that changes the product code. + /// + ///

+ /// Win32 MSI APIs: + /// MsiAdvertiseProduct, + /// MsiAdvertiseProductEx + ///

+ public static void GenerateAdvertiseScript( + string packagePath, + string scriptFilePath, + string transforms, + int locale, + ProcessorArchitecture processor, + bool instance) + { + if (string.IsNullOrWhiteSpace(packagePath)) + { + throw new ArgumentNullException("packagePath"); + } + + if (string.IsNullOrWhiteSpace(scriptFilePath)) + { + throw new ArgumentNullException("scriptFilePath"); + } + + if (!File.Exists(packagePath)) + { + throw new FileNotFoundException(null, packagePath); + } + + uint platform = 0; + switch (processor) + { + case ProcessorArchitecture.X86: platform = (uint) 1; break; + case ProcessorArchitecture.IA64: platform = (uint) 2; break; + case ProcessorArchitecture.Amd64: platform = (uint) 4; break; + } + + uint ret = NativeMethods.MsiAdvertiseProductEx( + packagePath, + scriptFilePath, + transforms, + (ushort) locale, + platform, + instance ? (uint) 1 : 0); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Copies an advertise script file to the local computer. + /// + /// Path to a script file generated by + /// + /// Flags controlling advertisement + /// True if specified items are to be removed instead of being created + ///

+ /// The process calling this function must be running under the LocalSystem account. To advertise an + /// application for per-user installation to a targeted user, the thread that calls this function must + /// impersonate the targeted user. If the thread calling this function is not impersonating a targeted + /// user, the application is advertised to all users for installation with elevated privileges. + ///

+ [SuppressMessage("Microsoft.Naming", "CA1726:UsePreferredTerms", MessageId = "flags")] + public static void AdvertiseScript(string scriptFile, int flags, bool removeItems) + { + uint ret = NativeMethods.MsiAdvertiseScript(scriptFile, (uint) flags, IntPtr.Zero, removeItems); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Processes an advertise script file into the specified locations. + /// + /// Path to a script file generated by + /// + /// An optional path to a folder in which advertised icon files and transform + /// files are located. If this parameter is null, no icon or transform files are written. + /// True if shortcuts should be created + /// True if specified items are to be removed instead of created + ///

+ /// The process calling this function must be running under the LocalSystem account. To advertise an + /// application for per-user installation to a targeted user, the thread that calls this function must + /// impersonate the targeted user. If the thread calling this function is not impersonating a targeted + /// user, the application is advertised to all users for installation with elevated privileges. + ///

+ /// Win32 MSI API: + /// MsiProcessAdvertiseScript + ///

+ public static void ProcessAdvertiseScript(string scriptFile, string iconFolder, bool shortcuts, bool removeItems) + { + uint ret = NativeMethods.MsiProcessAdvertiseScript(scriptFile, iconFolder, IntPtr.Zero, shortcuts, removeItems); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Gets product information for an installer script file. + /// + /// Path to a script file generated by + /// + /// ProductInstallation stub with advertise-related properties filled in. + /// An invalid product property was requested + ///

+ /// Only the following properties will be filled in in the returned object:

    + ///
  • + ///
  • + ///
  • + ///
  • + ///
  • + ///
Other properties will be null. + ///

+ /// Win32 MSI API: + /// MsiGetProductInfoFromScript + ///

+ public static ProductInstallation GetProductInfoFromScript(string scriptFile) + { + if (string.IsNullOrWhiteSpace(scriptFile)) + { + throw new ArgumentNullException("scriptFile"); + } + StringBuilder productCodeBuf = new StringBuilder(40); + ushort lang; + uint ver; + StringBuilder productNameBuf = new StringBuilder(100); + StringBuilder packageNameBuf = new StringBuilder(40); + uint productCodeBufSize = (uint) productCodeBuf.Capacity; + uint productNameBufSize = (uint) productNameBuf.Capacity; + uint packageNameBufSize = (uint) packageNameBuf.Capacity; + uint ret = NativeMethods.MsiGetProductInfoFromScript( + scriptFile, + productCodeBuf, + out lang, + out ver, + productNameBuf, + ref productNameBufSize, + packageNameBuf, + ref packageNameBufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + productCodeBuf.Capacity = (int) ++productCodeBufSize; + productNameBuf.Capacity = (int) ++productNameBufSize; + packageNameBuf.Capacity = (int) ++packageNameBufSize; + ret = NativeMethods.MsiGetProductInfoFromScript( + scriptFile, + productCodeBuf, + out lang, + out ver, + productNameBuf, + ref productNameBufSize, + packageNameBuf, + ref packageNameBufSize); + } + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + uint verPart1 = ver >> 24; + uint verPart2 = (ver & 0x00FFFFFF) >> 16; + uint verPart3 = ver & 0x0000FFFF; + Version version = new Version((int) verPart1, (int) verPart2, (int) verPart3); + + IDictionary props = new Dictionary(); + props["ProductCode"] = productCodeBuf.ToString(); + props["Language"] = lang.ToString(CultureInfo.InvariantCulture); + props["Version"] = version.ToString(); + props["ProductName"] = productNameBuf.ToString(); + props["PackageName"] = packageNameBuf.ToString(); + return new ProductInstallation(props); + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/InstallerUtils.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/InstallerUtils.cs new file mode 100644 index 000000000..697306a31 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/InstallerUtils.cs @@ -0,0 +1,478 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Resources; + using System.Runtime.InteropServices; + using System.Text; + + internal static partial class Installer + { + /// + /// Gets the current version of the installer. + /// + public static Version Version + { + get + { + // TODO: Use the extended form of version info to get the 4th component of the verison. + uint[] dllVersionInfo = new uint[5]; + dllVersionInfo[0] = 20; + int hr = NativeMethods.DllGetVersion(dllVersionInfo); + if (hr != 0) + { + Marshal.ThrowExceptionForHR(hr); + } + + return new Version((int) dllVersionInfo[1], (int) dllVersionInfo[2], (int) dllVersionInfo[3]); + } + } + + internal static ResourceManager ErrorResources + { + get + { + if (errorResources == null) + { + errorResources = new ResourceManager(typeof(Installer).Namespace + ".Errors", typeof(Installer).Assembly); + } + return errorResources; + } + } + + /// + /// Gets a Windows Installer error message in the system default language. + /// + /// The error number. + /// The message string, or null if the error message is not found. + ///

+ /// The returned string may have tokens such as [2] and [3] that are meant to be substituted + /// with context-specific values. + ///

+ /// Error numbers greater than 2000 refer to MSI "internal" errors, and are always + /// returned in English. + ///

+ public static string GetErrorMessage(int errorNumber) + { + return Installer.GetErrorMessage(errorNumber, null); + } + + /// + /// Gets a Windows Installer error message in a specified language. + /// + /// The error number. + /// The locale for the message. + /// The message string, or null if the error message or locale is not found. + ///

+ /// The returned string may have tokens such as [2] and [3] that are meant to be substituted + /// with context-specific values. + ///

+ /// Error numbers greater than 2000 refer to MSI "internal" errors, and are always + /// returned in English. + ///

+ [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")] + public static string GetErrorMessage(int errorNumber, CultureInfo culture) + { + if (culture == null) + { + culture = CultureInfo.CurrentUICulture; + } + + string msg = Installer.ErrorResources.GetString( + errorNumber.ToString(CultureInfo.InvariantCulture.NumberFormat), + culture); + if (msg == null) + { + string msiMsgModule = Path.Combine( + Environment.SystemDirectory, "msimsg.dll"); + msg = Installer.GetMessageFromModule( + msiMsgModule, errorNumber, culture); + } + return msg; + } + + private static string GetMessageFromModule( + string modulePath, int errorNumber, CultureInfo culture) + { + const uint LOAD_LIBRARY_AS_DATAFILE = 2; + const int RT_RCDATA = 10; + + IntPtr msgModule = NativeMethods.LoadLibraryEx( + modulePath, IntPtr.Zero, LOAD_LIBRARY_AS_DATAFILE); + if (msgModule == IntPtr.Zero) + { + return null; + } + + try + { + // On pre-Vista systems, the messages are stored as RCDATA resources. + + int lcid = (culture == CultureInfo.InvariantCulture) ? + 0 : culture.LCID; + IntPtr resourceInfo = NativeMethods.FindResourceEx( + msgModule, + new IntPtr(RT_RCDATA), + new IntPtr(errorNumber), + (ushort) lcid); + if (resourceInfo != IntPtr.Zero) + { + IntPtr resourceData = NativeMethods.LoadResource( + msgModule, resourceInfo); + IntPtr resourcePtr = NativeMethods.LockResource(resourceData); + + if (lcid == 0) + { + string msg = Marshal.PtrToStringAnsi(resourcePtr); + return msg; + } + else + { + int len = 0; + while (Marshal.ReadByte(resourcePtr, len) != 0) + { + len++; + } + byte[] msgBytes = new byte[len + 1]; + Marshal.Copy(resourcePtr, msgBytes, 0, msgBytes.Length); + Encoding encoding = Encoding.GetEncoding( + culture.TextInfo.ANSICodePage); + string msg = encoding.GetString(msgBytes); + return msg; + } + } + else + { + // On Vista (and above?), the messages are stored in the module message table. + // They're actually in MUI files, and the redirection happens automatically here. + + const uint FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200; + const uint FORMAT_MESSAGE_FROM_HMODULE = 0x00000800; + const uint MESSAGE_OFFSET = 20000; // Not documented, but observed on Vista + + StringBuilder buf = new StringBuilder(1024); + uint formatCount = NativeMethods.FormatMessage( + FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS, + msgModule, + (uint) errorNumber + MESSAGE_OFFSET, + (ushort) lcid, + buf, + (uint) buf.Capacity, + IntPtr.Zero); + + return formatCount != 0 ? buf.ToString().Trim() : null; + } + } + finally + { + NativeMethods.FreeLibrary(msgModule); + } + } + + /// + /// Gets a formatted Windows Installer error message in the system default language. + /// + /// Error record containing the error number in the first field, and + /// error-specific parameters in the other fields. + /// The message string, or null if the error message is not found. + ///

+ /// Error numbers greater than 2000 refer to MSI "internal" errors, and are always + /// returned in English. + ///

+ public static string GetErrorMessage(Record errorRecord) { return Installer.GetErrorMessage(errorRecord, null); } + + /// + /// Gets a formatted Windows Installer error message in a specified language. + /// + /// Error record containing the error number in the first field, and + /// error-specific parameters in the other fields. + /// The locale for the message. + /// The message string, or null if the error message or locale is not found. + ///

+ /// Error numbers greater than 2000 refer to MSI "internal" errors, and are always + /// returned in English. + ///

+ public static string GetErrorMessage(Record errorRecord, CultureInfo culture) + { + if (errorRecord == null) + { + throw new ArgumentNullException("errorRecord"); + } + int errorNumber; + if (errorRecord.FieldCount < 1 || (errorNumber = (int) errorRecord.GetInteger(1)) == 0) + { + throw new ArgumentOutOfRangeException("errorRecord"); + } + + string msg = Installer.GetErrorMessage(errorNumber, culture); + if (msg != null) + { + errorRecord.FormatString = msg; + msg = errorRecord.ToString((IFormatProvider)null); + } + return msg; + } + + /// + /// Gets the version string of the path specified using the format that the installer + /// expects to find it in in the database. + /// + /// Path to the file + /// Version string in the "#.#.#.#" format, or an empty string if the file + /// does not contain version information + /// the file does not exist or could not be read + ///

+ /// Win32 MSI API: + /// MsiGetFileVersion + ///

+ public static string GetFileVersion(string path) + { + StringBuilder version = new StringBuilder(20); + uint verBufSize = 0, langBufSize = 0; + uint ret = NativeMethods.MsiGetFileVersion(path, version, ref verBufSize, null, ref langBufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + version.Capacity = (int) ++verBufSize; + ret = NativeMethods.MsiGetFileVersion(path, version, ref verBufSize, null, ref langBufSize); + } + + if (ret != 0 && ret != (uint) NativeMethods.Error.FILE_INVALID) + { + if (ret == (uint) NativeMethods.Error.FILE_NOT_FOUND || + ret == (uint) NativeMethods.Error.ACCESS_DENIED) + { + throw new FileNotFoundException(null, path); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + return version.ToString(); + } + + /// + /// Gets the language string of the path specified using the format that the installer + /// expects to find them in in the database. + /// + /// Path to the file + /// Language string in the form of a decimal language ID, or an empty string if the file + /// does not contain a language ID + /// the file does not exist or could not be read + ///

+ /// Win32 MSI API: + /// MsiGetFileVersion + ///

+ public static string GetFileLanguage(string path) + { + StringBuilder language = new StringBuilder("", 10); + uint verBufSize = 0, langBufSize = 0; + uint ret = NativeMethods.MsiGetFileVersion(path, null, ref verBufSize, language, ref langBufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + language.Capacity = (int) ++langBufSize; + ret = NativeMethods.MsiGetFileVersion(path, null, ref verBufSize, language, ref langBufSize); + } + + if (ret != 0 && ret != (uint) NativeMethods.Error.FILE_INVALID) + { + if (ret == (uint) NativeMethods.Error.FILE_NOT_FOUND || + ret == (uint) NativeMethods.Error.ACCESS_DENIED) + { + throw new FileNotFoundException(null, path); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + return language.ToString(); + } + + /// + /// Gets a 128-bit hash of the specified file. + /// + /// Path to the file + /// Integer array of length 4 which receives the + /// four 32-bit parts of the hash value. + /// the file does not exist or + /// could not be read + ///

+ /// Win32 MSI API: + /// MsiGetFileHash + ///

+ public static void GetFileHash(string path, int[] hash) + { + if (hash == null) + { + throw new ArgumentNullException("hash"); + } + + uint[] tempHash = new uint[5]; + tempHash[0] = 20; + uint ret = NativeMethods.MsiGetFileHash(path, 0, tempHash); + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.FILE_NOT_FOUND || + ret == (uint) NativeMethods.Error.ACCESS_DENIED) + { + throw new FileNotFoundException(null, path); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + for (int i = 0; i < 4; i++) + { + hash[i] = unchecked ((int) tempHash[i + 1]); + } + } + + /// + /// Examines a shortcut and returns its product, feature name, and component if available. + /// + /// Full path to a shortcut + /// ShortcutTarget structure containing target product code, feature, and component code + ///

+ /// Win32 MSI API: + /// MsiGetShortcutTarget + ///

+ public static ShortcutTarget GetShortcutTarget(string shortcut) + { + StringBuilder productBuf = new StringBuilder(40); + StringBuilder featureBuf = new StringBuilder(40); + StringBuilder componentBuf = new StringBuilder(40); + + uint ret = NativeMethods.MsiGetShortcutTarget(shortcut, productBuf, featureBuf, componentBuf); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return new ShortcutTarget( + productBuf.Length > 0 ? productBuf.ToString() : null, + featureBuf.Length > 0 ? featureBuf.ToString() : null, + componentBuf.Length > 0 ? componentBuf.ToString() : null); + } + + /// + /// Verifies that the given file is an installation package. + /// + /// Path to the package + /// True if the file is an installation package; false otherwise. + /// the specified package file does not exist + /// the package file could not be opened + ///

+ /// Win32 MSI API: + /// MsiVerifyPackage + ///

+ public static bool VerifyPackage(string packagePath) + { + if (string.IsNullOrWhiteSpace(packagePath)) + { + throw new ArgumentNullException("packagePath"); + } + + if (!File.Exists(packagePath)) + { + throw new FileNotFoundException(null, packagePath); + } + + uint ret = NativeMethods.MsiVerifyPackage(packagePath); + if (ret == (uint) NativeMethods.Error.INSTALL_PACKAGE_INVALID) + { + return false; + } + else if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return true; + } + + /// + /// [MSI 4.0] Gets the list of files that can be updated by one or more patches. + /// + /// ProductCode (GUID) of the product which is + /// the target of the patches + /// list of file paths of one or more patches to be + /// analyzed + /// List of absolute paths of files that can be updated when the + /// patches are applied on this system. + ///

+ /// Win32 MSI API: + /// MsiGetPatchFileList + ///

+ public static IList GetPatchFileList(string productCode, IList patches) + { + if (string.IsNullOrWhiteSpace(productCode)) + { + throw new ArgumentNullException("productCode"); + } + + if (patches == null || patches.Count == 0) + { + throw new ArgumentNullException("patches"); + } + + StringBuilder patchList = new StringBuilder(); + foreach (string patch in patches) + { + if (patch != null) + { + if (patchList.Length != 0) + { + patchList.Append(';'); + } + + patchList.Append(patch); + } + } + + if (patchList.Length == 0) + { + throw new ArgumentNullException("patches"); + } + + IntPtr phFileRecords; + uint cFiles; + + uint ret = NativeMethods.MsiGetPatchFileList( + productCode, + patchList.ToString(), + out cFiles, + out phFileRecords); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + List files = new List(); + + for (uint i = 0; i < cFiles; i++) + { + int hFileRec = Marshal.ReadInt32(phFileRecords, (int) i); + + using (Record fileRec = new Record(hFileRec, true, null)) + { + files.Add(fileRec.GetString(1)); + } + } + + return files; + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/MediaDisk.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/MediaDisk.cs new file mode 100644 index 000000000..bc08c9850 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/MediaDisk.cs @@ -0,0 +1,64 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Microsoft.Deployment.WindowsInstaller.MediaDisk struct. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System.Diagnostics.CodeAnalysis; + + /// + /// Represents a media disk source of a product or a patch. + /// + [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes")] + public struct MediaDisk + { + private int diskId; + private string volumeLabel; + private string diskPrompt; + + /// + /// Creates a new media disk. + /// + /// + /// + /// + public MediaDisk(int diskId, string volumeLabel, string diskPrompt) + { + this.diskId = diskId; + this.volumeLabel = volumeLabel; + this.diskPrompt = diskPrompt; + } + + /// + /// Gets or sets the disk id of the media disk. + /// + public int DiskId + { + get { return this.diskId; } + set { this.diskId = value; } + } + + /// + /// Gets or sets the volume label of the media disk. + /// + public string VolumeLabel + { + get { return this.volumeLabel; } + set { this.volumeLabel = value; } + } + + /// + /// Gets or sets the disk prompt of the media disk. + /// + public string DiskPrompt + { + get { return this.diskPrompt; } + set { this.diskPrompt = value; } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/NativeMethods.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/NativeMethods.cs new file mode 100644 index 000000000..010243a52 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/NativeMethods.cs @@ -0,0 +1,317 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Runtime.InteropServices; + using System.Runtime.InteropServices.ComTypes; + using System.Security.Permissions; + using System.Text; + using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME; + using STATSTG = System.Runtime.InteropServices.ComTypes.STATSTG; + + [Guid("0000000b-0000-0000-C000-000000000046")] +[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] +internal interface IStorage +{ + [return: MarshalAs(UnmanagedType.Interface)] + IStream CreateStream([MarshalAs(UnmanagedType.LPWStr)] string wcsName, uint grfMode, uint reserved1, uint reserved2); + [return: MarshalAs(UnmanagedType.Interface)] + IStream OpenStream([MarshalAs(UnmanagedType.LPWStr)] string wcsName, IntPtr reserved1, uint grfMode, uint reserved2); + [return: MarshalAs(UnmanagedType.Interface)] + IStorage CreateStorage([MarshalAs(UnmanagedType.LPWStr)] string wcsName, uint grfMode, uint reserved1, uint reserved2); + [return: MarshalAs(UnmanagedType.Interface)] + IStorage OpenStorage([MarshalAs(UnmanagedType.LPWStr)] string wcsName, IntPtr stgPriority, uint grfMode, IntPtr snbExclude, uint reserved); + void CopyTo(uint ciidExclude, IntPtr rgiidExclude, IntPtr snbExclude, [MarshalAs(UnmanagedType.Interface)] IStorage stgDest); + void MoveElementTo([MarshalAs(UnmanagedType.LPWStr)] string wcsName, [MarshalAs(UnmanagedType.Interface)] IStorage stgDest, [MarshalAs(UnmanagedType.LPWStr)] string wcsNewName, uint grfFlags); + void Commit(uint grfCommitFlags); + void Revert(); + IntPtr EnumElements(uint reserved1, IntPtr reserved2, uint reserved3); + void DestroyElement([MarshalAs(UnmanagedType.LPWStr)] string wcsName); + void RenameElement([MarshalAs(UnmanagedType.LPWStr)] string wcsOldName, [MarshalAs(UnmanagedType.LPWStr)] string wcsNewName); + void SetElementTimes([MarshalAs(UnmanagedType.LPWStr)] string wcsName, ref FILETIME ctime, ref FILETIME atime, ref FILETIME mtime); + void SetClass(ref Guid clsid); + void SetStateBits(uint grfStateBits, uint grfMask); + void Stat(ref STATSTG statstg, uint grfStatFlag); +} + +internal static class NativeMethods +{ + internal enum Error : uint + { + SUCCESS = 0, + FILE_NOT_FOUND = 2, + PATH_NOT_FOUND = 3, + ACCESS_DENIED = 5, + INVALID_HANDLE = 6, + INVALID_DATA = 13, + INVALID_PARAMETER = 87, + OPEN_FAILED = 110, + DISK_FULL = 112, + CALL_NOT_IMPLEMENTED = 120, + BAD_PATHNAME = 161, + NO_DATA = 232, + MORE_DATA = 234, + NO_MORE_ITEMS = 259, + DIRECTORY = 267, + INSTALL_USEREXIT = 1602, + INSTALL_FAILURE = 1603, + FILE_INVALID = 1006, + UNKNOWN_PRODUCT = 1605, + UNKNOWN_FEATURE = 1606, + UNKNOWN_COMPONENT = 1607, + UNKNOWN_PROPERTY = 1608, + INVALID_HANDLE_STATE = 1609, + INSTALL_SOURCE_ABSENT = 1612, + BAD_QUERY_SYNTAX = 1615, + INSTALL_PACKAGE_INVALID = 1620, + FUNCTION_FAILED = 1627, + INVALID_TABLE = 1628, + DATATYPE_MISMATCH = 1629, + CREATE_FAILED = 1631, + SUCCESS_REBOOT_INITIATED = 1641, + SUCCESS_REBOOT_REQUIRED = 3010, + } + + internal enum SourceType : int + { + Unknown = 0, + Network = 1, + Url = 2, + Media = 3, + } + + [Flags] + internal enum STGM : uint + { + DIRECT = 0x00000000, + TRANSACTED = 0x00010000, + SIMPLE = 0x08000000, + + READ = 0x00000000, + WRITE = 0x00000001, + READWRITE = 0x00000002, + + SHARE_DENY_NONE = 0x00000040, + SHARE_DENY_READ = 0x00000030, + SHARE_DENY_WRITE = 0x00000020, + SHARE_EXCLUSIVE = 0x00000010, + + PRIORITY = 0x00040000, + DELETEONRELEASE = 0x04000000, + NOSCRATCH = 0x00100000, + + CREATE = 0x00001000, + CONVERT = 0x00020000, + FAILIFTHERE = 0x00000000, + + NOSNAPSHOT = 0x00200000, + DIRECT_SWMR = 0x00400000, + } + +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int DllGetVersion(uint[] dvi); + +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetInternalUI(uint dwUILevel, ref IntPtr phWnd); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetInternalUI(uint dwUILevel, IntPtr phWnd); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern NativeExternalUIHandler MsiSetExternalUI([MarshalAs(UnmanagedType.FunctionPtr)] NativeExternalUIHandler puiHandler, uint dwMessageFilter, IntPtr pvContext); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnableLog(uint dwLogMode, string szLogFile, uint dwLogAttributes); +//[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumProducts(uint iProductIndex, StringBuilder lpProductBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetProductInfo(string szProduct, string szProperty, StringBuilder lpValueBuf, ref uint pcchValueBuf); +//[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumPatches(string szProduct, uint iPatchIndex, StringBuilder lpPatchBuf, StringBuilder lpTransformsBuf, ref uint pcchTransformsBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetPatchInfo(string szPatch, string szAttribute, StringBuilder lpValueBuf, ref uint pcchValueBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumFeatures(string szProduct, uint iFeatureIndex, StringBuilder lpFeatureBuf, StringBuilder lpParentBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiQueryFeatureState(string szProduct, string szFeature); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiUseFeatureEx(string szProduct, string szFeature, uint dwInstallMode, uint dwReserved); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiQueryProductState(string szProduct); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetShortcutTarget(string szShortcut, StringBuilder szProductCode, StringBuilder szFeatureId, StringBuilder szComponentCode); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiProvideComponent(string szProduct, string szFeature, string szComponent, uint dwInstallMode, StringBuilder lpPathBuf, ref uint cchPathBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiProvideQualifiedComponentEx(string szComponent, string szQualifier, uint dwInstallMode, string szProduct, uint dwUnused1, uint dwUnused2, StringBuilder lpPathBuf, ref uint cchPathBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiReinstallFeature(string szFeature, string szProduct, uint dwReinstallMode); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiReinstallProduct(string szProduct, uint dwReinstallMode); +//[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListAddSource(string szProduct, string szUserName, uint dwReserved, string szSource); +//[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListClearAll(string szProduct, string szUserName, uint dwReserved); +//[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListForceResolution(string szProduct, string szUserName, uint dwReserved); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiCollectUserInfo(string szProduct); +//[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiGetUserInfo(string szProduct, StringBuilder lpUserNameBuf, ref uint cchUserNameBuf, StringBuilder lpOrgNameBuf, ref uint cchOrgNameBuf, StringBuilder lpSerialBuf, ref uint cchSerialBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiOpenPackageEx(string szPackagePath, uint dwOptions, out int hProduct); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiOpenProduct(string szProduct, out int hProduct); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiInstallProduct(string szPackagePath, string szCommandLine); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiConfigureProductEx(string szProduct, int iInstallLevel, int eInstallState, string szCommandLine); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiConfigureFeature(string szProduct, string szFeature, int eInstallState); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiApplyPatch(string szPatchPackage, string szInstallPackage, int eInstallType, string szCommandLine); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiOpenDatabase(string szDatabasePath, IntPtr uiOpenMode, out int hDatabase); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiOpenDatabase(string szDatabasePath, string szPersist, out int hDatabase); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiGetDatabaseState(int hDatabase); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseOpenView(int hDatabase, string szQuery, out int hView); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseMerge(int hDatabase, int hDatabaseMerge, string szTableName); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseCommit(int hDatabase); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseGetPrimaryKeys(int hDatabase, string szTableName, out int hRecord); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseIsTablePersistent(int hDatabase, string szTableName); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseExport(int hDatabase, string szTableName, string szFolderPath, string szFileName); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseImport(int hDatabase, string szFolderPath, string szFileName); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseGenerateTransform(int hDatabase, int hDatabaseReference, string szTransformFile, int iReserved1, int iReserved2); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiCreateTransformSummaryInfo(int hDatabase, int hDatabaseReference, string szTransformFile, int iErrorConditions, int iValidation); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDatabaseApplyTransform(int hDatabase, string szTransformFile, int iErrorConditions); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiViewExecute(int hView, int hRecord); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiViewFetch(int hView, out int hRecord); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiViewModify(int hView, int iModifyMode, int hRecord); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiViewGetError(int hView, StringBuilder szColumnNameBuffer, ref uint cchBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiViewGetColumnInfo(int hView, uint eColumnInfo, out int hRecord); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiCreateRecord(uint cParams); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiFormatRecord(int hInstall, int hRecord, StringBuilder szResultBuf, ref uint cchResultBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRecordClearData(int hRecord); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRecordGetFieldCount(int hRecord); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool MsiRecordIsNull(int hRecord, uint iField); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiRecordGetInteger(int hRecord, uint iField); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRecordGetString(int hRecord, uint iField, StringBuilder szValueBuf, ref uint cchValueBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRecordSetInteger(int hRecord, uint iField, int iValue); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRecordSetString(int hRecord, uint iField, string szValue); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRecordDataSize(int hRecord, uint iField); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRecordReadStream(int hRecord, uint iField, byte[] szDataBuf, ref uint cbDataBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRecordSetStream(int hRecord, uint iField, string szFilePath); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetSummaryInformation(int hDatabase, string szDatabasePath, uint uiUpdateCount, out int hSummaryInfo); +//[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSummaryInfoGetPropertyCount(int hSummaryInfo, out uint uiPropertyCount); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSummaryInfoGetProperty(int hSummaryInfo, uint uiProperty, out uint uiDataType, out int iValue, ref long ftValue, StringBuilder szValueBuf, ref uint cchValueBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSummaryInfoSetProperty(int hSummaryInfo, uint uiProperty, uint uiDataType, int iValue, ref long ftValue, string szValue); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSummaryInfoPersist(int hSummaryInfo); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiCloseHandle(int hAny); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetFileVersion(string szFilePath, StringBuilder szVersionBuf, ref uint cchVersionBuf, StringBuilder szLangBuf, ref uint cchLangBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetFileHash(string szFilePath, uint dwOptions, uint[] hash); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiGetActiveDatabase(int hInstall); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetProperty(int hInstall, string szName, StringBuilder szValueBuf, ref uint cchValueBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetProperty(int hInstall, string szName, string szValue); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiProcessMessage(int hInstall, uint eMessageType, int hRecord); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEvaluateCondition(int hInstall, string szCondition); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool MsiGetMode(int hInstall, uint iRunMode); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetMode(int hInstall, uint iRunMode, [MarshalAs(UnmanagedType.Bool)] bool fState); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDoAction(int hInstall, string szAction); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSequence(int hInstall, string szTable, int iSequenceMode); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetSourcePath(int hInstall, string szFolder, StringBuilder szPathBuf, ref uint cchPathBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetTargetPath(int hInstall, string szFolder, StringBuilder szPathBuf, ref uint cchPathBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetTargetPath(int hInstall, string szFolder, string szFolderPath); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetComponentState(int hInstall, string szComponent, out int iInstalled, out int iAction); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetComponentState(int hInstall, string szComponent, int iState); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetFeatureState(int hInstall, string szFeature, out int iInstalled, out int iAction); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetFeatureState(int hInstall, string szFeature, int iState); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetFeatureValidStates(int hInstall, string szFeature, out uint dwInstallState); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetInstallLevel(int hInstall, int iInstallLevel); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern ushort MsiGetLanguage(int hInstall); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumComponents(uint iComponentIndex, StringBuilder lpComponentBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumComponentsEx(string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwIndex, StringBuilder szInstalledProductCode, [MarshalAs(UnmanagedType.I4)] out UserContexts pdwInstalledContext, StringBuilder szSid, ref uint pcchSid); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumClients(string szComponent, uint iProductIndex, StringBuilder lpProductBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumClientsEx(string szComponent, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint iProductIndex, StringBuilder lpProductBuf, [MarshalAs(UnmanagedType.I4)] out UserContexts pdwInstalledContext, StringBuilder szSid, ref uint pcchSid); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiGetComponentPath(string szProduct, string szComponent, StringBuilder lpPathBuf, ref uint pcchBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiGetComponentPathEx(string szProduct, string szComponent, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, StringBuilder lpPathBuf, ref uint pcchBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumComponentQualifiers(string szComponent, uint iIndex, StringBuilder lpQualifierBuf, ref uint pcchQualifierBuf, StringBuilder lpApplicationDataBuf, ref uint pcchApplicationDataBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern int MsiGetLastErrorRecord(); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumRelatedProducts(string upgradeCode, uint dwReserved, uint iProductIndex, StringBuilder lpProductBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetProductCode(string szComponent, StringBuilder lpProductBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetFeatureUsage(string szProduct, string szFeature, out uint dwUseCount, out ushort dwDateUsed); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetFeatureCost(int hInstall, string szFeature, int iCostTree, int iState, out int iCost); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiVerifyPackage(string szPackagePath); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiIsProductElevated(string szProductCode, [MarshalAs(UnmanagedType.Bool)] out bool fElevated); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiAdvertiseProduct(string szPackagePath, IntPtr szScriptFilePath, string szTransforms, ushort lgidLanguage); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiAdvertiseProduct(string szPackagePath, string szScriptFilePath, string szTransforms, ushort lgidLanguage); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiAdvertiseProductEx(string szPackagePath, string szScriptFilePath, string szTransforms, ushort lgidLanguage, uint dwPlatform, uint dwReserved); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiAdvertiseScript(string szScriptFile, uint dwFlags, IntPtr phRegData, [MarshalAs(UnmanagedType.Bool)] bool fRemoveItems); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiProcessAdvertiseScript(string szScriptFile, string szIconFolder, IntPtr hRegData, [MarshalAs(UnmanagedType.Bool)] bool fShortcuts, [MarshalAs(UnmanagedType.Bool)] bool fRemoveItems); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetProductInfoFromScript(string szScriptFile, StringBuilder lpProductBuf39, out ushort plgidLanguage, out uint pdwVersion, StringBuilder lpNameBuf, ref uint cchNameBuf, StringBuilder lpPackageBuf, ref uint cchPackageBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiProvideAssembly(string szAssemblyName, string szAppContext, uint dwInstallMode, uint dwAssemblyInfo, StringBuilder lpPathBuf, ref uint cchPathBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiInstallMissingComponent(string szProduct, string szComponent, int eInstallState); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiInstallMissingFile(string szProduct, string szFile); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiLocateComponent(string szComponent, StringBuilder lpPathBuf, ref uint cchBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetProductProperty(int hProduct, string szProperty, StringBuilder lpValueBuf, ref uint cchValueBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetFeatureInfo(int hProduct, string szFeature, out uint lpAttributes, StringBuilder lpTitleBuf, ref uint cchTitleBuf, StringBuilder lpHelpBuf, ref uint cchHelpBuf); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiVerifyDiskSpace(int hInstall); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumComponentCosts(int hInstall, string szComponent, uint dwIndex, int iState, StringBuilder lpDriveBuf, ref uint cchDriveBuf, out int iCost, out int iTempCost); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetFeatureAttributes(int hInstall, string szFeature, uint dwAttributes); + +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiRemovePatches(string szPatchList, string szProductCode, int eUninstallType, string szPropertyList); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDetermineApplicablePatches(string szProductPackagePath, uint cPatchInfo, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=1), In, Out] MsiPatchSequenceData[] pPatchInfo); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiDeterminePatchSequence(string szProductCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint cPatchInfo, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=3), In, Out] MsiPatchSequenceData[] pPatchInfo); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiApplyMultiplePatches(string szPatchPackages, string szProductCode, string szPropertiesList); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumPatchesEx(string szProductCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwFilter, uint dwIndex, StringBuilder szPatchCode, StringBuilder szTargetProductCode, [MarshalAs(UnmanagedType.I4)] out UserContexts pdwTargetProductContext, StringBuilder szTargetUserSid, ref uint pcchTargetUserSid); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetPatchInfoEx(string szPatchCode, string szProductCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, string szProperty, StringBuilder lpValue, ref uint pcchValue); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEnumProductsEx(string szProductCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwIndex, StringBuilder szInstalledProductCode, [MarshalAs(UnmanagedType.I4)] out UserContexts pdwInstalledContext, StringBuilder szSid, ref uint pcchSid); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetProductInfoEx(string szProductCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, string szProperty, StringBuilder lpValue, ref uint pcchValue); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiQueryFeatureStateEx(string szProductCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, string szFeature, out int pdwState); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiQueryComponentState(string szProductCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, string szComponent, out int pdwState); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiExtractPatchXMLData(string szPatchPath, uint dwReserved, StringBuilder szXMLData, ref uint pcchXMLData); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListEnumSources(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions, uint dwIndex, StringBuilder szSource, ref uint pcchSource); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListAddSourceEx(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions, string szSource, uint dwIndex); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListClearSource(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions, string szSource); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListClearAllEx(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListForceResolutionEx(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListGetInfo(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions, string szProperty, StringBuilder szValue, ref uint pcchValue); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListSetInfo(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions, string szProperty, string szValue); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListEnumMediaDisks(string szProductCodeOrPatchCode, string szUserSID, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions, uint dwIndex, out uint pdwDiskId, StringBuilder szVolumeLabel, ref uint pcchVolumeLabel, StringBuilder szDiskPrompt, ref uint pcchDiskPrompt); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListAddMediaDisk(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions, uint dwDiskId, string szVolumeLabel, string szDiskPrompt); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSourceListClearMediaDisk(string szProductCodeOrPatchCode, string szUserSid, [MarshalAs(UnmanagedType.I4)] UserContexts dwContext, uint dwOptions, uint dwDiskID); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiNotifySidChange(string szOldSid, string szNewSid); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiSetExternalUIRecord([MarshalAs(UnmanagedType.FunctionPtr)] NativeExternalUIRecordHandler puiHandler, uint dwMessageFilter, IntPtr pvContext, out NativeExternalUIRecordHandler ppuiPrevHandler); + +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiGetPatchFileList(string szProductCode, string szPatchList, out uint cFiles, out IntPtr phFileRecords); + +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiBeginTransaction(string szTransactionName, int dwTransactionAttributes, out int hTransaction, out IntPtr phChangeOfOwnerEvent); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiEndTransaction(int dwTransactionState); +[DllImport("msi.dll", CharSet=CharSet.Unicode)] internal static extern uint MsiJoinTransaction(int hTransaction, int dwTransactionAttributes, out IntPtr phChangeOfOwnerEvent); + +[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode, EntryPoint="LoadLibraryExW")] internal static extern IntPtr LoadLibraryEx(string fileName, IntPtr hFile, uint flags); +[DllImport("kernel32.dll", SetLastError=true)] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool FreeLibrary(IntPtr hModule); +[DllImport("kernel32.dll", SetLastError=true)] internal static extern IntPtr FindResourceEx(IntPtr hModule, IntPtr type, IntPtr name, ushort langId); +[DllImport("kernel32.dll", SetLastError=true)] internal static extern IntPtr LoadResource(IntPtr hModule, IntPtr lpResourceInfo); +[DllImport("kernel32.dll", SetLastError=true)] internal static extern IntPtr LockResource(IntPtr resourceData); +[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode, EntryPoint="FormatMessageW")] internal static extern uint FormatMessage(uint dwFlags, IntPtr lpSource, uint dwMessageId, uint dwLanguageId, StringBuilder lpBuffer, uint nSize, IntPtr Arguments); +[DllImport("kernel32.dll", SetLastError=true)] internal static extern int WaitForSingleObject(IntPtr handle, int milliseconds); + +[DllImport("ole32.dll")] internal static extern int StgOpenStorage([MarshalAs(UnmanagedType.LPWStr)] string wcsName, IntPtr stgPriority, uint grfMode, IntPtr snbExclude, uint reserved, [MarshalAs(UnmanagedType.Interface)] out IStorage stgOpen); +[DllImport("ole32.dll")] internal static extern int StgCreateDocfile([MarshalAs(UnmanagedType.LPWStr)] string wcsName, uint grfMode, uint reserved, [MarshalAs(UnmanagedType.Interface)] out IStorage stgOpen); + +[DllImport("user32.dll", CharSet=CharSet.Unicode, EntryPoint="MessageBoxW")] internal static extern MessageResult MessageBox(IntPtr hWnd, string lpText, string lpCaption, [MarshalAs(UnmanagedType.U4)] int uType); + + [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] + internal struct MsiPatchSequenceData + { + public string szPatchData; + public int ePatchDataType; + public int dwOrder; + public uint dwStatus; + } + + internal class MsiHandle : SafeHandle + { + [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")] + public MsiHandle(IntPtr handle, bool ownsHandle) + : base(handle, ownsHandle) + { + } + + public override bool IsInvalid + { + get + { + return this.handle == IntPtr.Zero; + } + } + + public static implicit operator IntPtr(MsiHandle msiHandle) + { + return msiHandle.handle; + } + + [SecurityPermission(SecurityAction.Assert, UnmanagedCode = true)] + protected override bool ReleaseHandle() + { + return RemotableNativeMethods.MsiCloseHandle((int) this.handle) == 0; + } + } +} +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/PatchInstallation.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/PatchInstallation.cs new file mode 100644 index 000000000..e0ab88221 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/PatchInstallation.cs @@ -0,0 +1,430 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Text; + + /// + /// The Patch object represents a unique instance of a patch that has been + /// registered or applied. + /// + internal class PatchInstallation : Installation + { + /// + /// Enumerates all patch installations on the system. + /// + /// Enumeration of patch objects. + ///

+ /// Win32 MSI API: + /// MsiEnumPatches + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static IEnumerable AllPatches + { + get + { + return PatchInstallation.GetPatches(null, null, null, UserContexts.All, PatchStates.All); + } + } + + /// + /// Enumerates patch installations based on certain criteria. + /// + /// PatchCode (GUID) of the patch to be enumerated. Only + /// instances of patches within the scope of the context specified by the + /// and parameters will be + /// enumerated. This parameter may be set to null to enumerate all patches in the specified + /// context. + /// ProductCode (GUID) product whose patches are to be + /// enumerated. If non-null, patch enumeration is restricted to instances of this product + /// within the specified context. If null, the patches for all products under the specified + /// context are enumerated. + /// Specifies a security identifier (SID) that restricts the context + /// of enumeration. A SID value other than s-1-1-0 is considered a user SID and restricts + /// enumeration to the current user or any user in the system. The special SID string + /// s-1-1-0 (Everyone) specifies enumeration across all users in the system. This parameter + /// can be set to null to restrict the enumeration scope to the current user. When + /// must be null. + /// Specifies the user context. + /// The of patches to return. + ///

+ /// Win32 MSI APIs: + /// MsiEnumPatchesEx + ///

+ public static IEnumerable GetPatches( + string patchCode, + string targetProductCode, + string userSid, + UserContexts context, + PatchStates states) + { + StringBuilder buf = new StringBuilder(40); + StringBuilder targetProductBuf = new StringBuilder(40); + UserContexts targetContext; + StringBuilder targetSidBuf = new StringBuilder(40); + for (uint i = 0; ; i++) + { + uint targetSidBufSize = (uint) targetSidBuf.Capacity; + uint ret = NativeMethods.MsiEnumPatchesEx( + targetProductCode, + userSid, + context, + (uint) states, + i, + buf, + targetProductBuf, + out targetContext, + targetSidBuf, + ref targetSidBufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + targetSidBuf.Capacity = (int) ++targetSidBufSize; + ret = NativeMethods.MsiEnumPatchesEx( + targetProductCode, + userSid, + context, + (uint) states, + i, + buf, + targetProductBuf, + out targetContext, + targetSidBuf, + ref targetSidBufSize); + } + + if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) + { + break; + } + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + string thisPatchCode = buf.ToString(); + if (patchCode == null || patchCode == thisPatchCode) + { + yield return new PatchInstallation( + buf.ToString(), + targetProductBuf.ToString(), + targetSidBuf.ToString(), + targetContext); + } + } + } + + private string productCode; + + /// + /// Creates a new object for accessing information about a patch installation on the current system. + /// + /// Patch code (GUID) of the patch. + /// ProductCode (GUID) the patch has been applied to. + /// This parameter may be null for patches that are registered only and not yet + /// applied to any product. + ///

+ /// All available user contexts will be queried. + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public PatchInstallation(string patchCode, string productCode) + : this(patchCode, productCode, null, UserContexts.All) + { + } + + /// + /// Creates a new object for accessing information about a patch installation on the current system. + /// + /// Registered patch code (GUID) of the patch. + /// ProductCode (GUID) the patch has been applied to. + /// This parameter may be null for patches that are registered only and not yet + /// applied to any product. + /// The specific user, when working in a user context. This + /// parameter may be null to indicate the current user. The parameter must be null + /// when working in a machine context. + /// The user context. The calling process must have administrative + /// privileges to get information for a product installed for a user other than the + /// current user. + ///

+ /// If the is null, the Patch object may + /// only be used to read and update the patch's SourceList information. + ///

+ public PatchInstallation(string patchCode, string productCode, string userSid, UserContexts context) + : base(patchCode, userSid, context) + { + if (string.IsNullOrWhiteSpace(patchCode)) + { + throw new ArgumentNullException("patchCode"); + } + + this.productCode = productCode; + } + + /// + /// Gets the patch code (GUID) of the patch. + /// + public string PatchCode + { + get + { + return this.InstallationCode; + } + } + + /// + /// Gets the ProductCode (GUID) of the product. + /// + public string ProductCode + { + get + { + return this.productCode; + } + } + + /// + /// Gets a value indicating whether this patch is currently installed. + /// + public override bool IsInstalled + { + get + { + return (this.State & PatchStates.Applied) != 0; + } + } + + /// + /// Gets a value indicating whether this patch is marked as obsolte. + /// + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Obsoleted")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool IsObsoleted + { + get + { + return (this.State & PatchStates.Obsoleted) != 0; + } + } + + /// + /// Gets a value indicating whether this patch is present but has been + /// superseded by a more recent installed patch. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool IsSuperseded + { + get + { + return (this.State & PatchStates.Superseded) != 0; + } + } + + internal override int InstallationType + { + get + { + const int MSICODE_PATCH = 0x40000000; + return MSICODE_PATCH; + } + } + + /// + /// Gets the installation state of this instance of the patch. + /// + /// An unknown patch was requested + /// The installer configuration data is corrupt + public PatchStates State + { + get + { + string stateString = this["State"]; + return (PatchStates) Int32.Parse(stateString, CultureInfo.InvariantCulture.NumberFormat); + } + } + + /// + /// Gets the cached patch file that the product uses. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string LocalPackage + { + get + { + return this["LocalPackage"]; + } + } + + /// + /// Gets the set of patch transforms that the last patch + /// installation applied to the product. + /// + ///

+ /// This value may not be available for per-user, non-managed applications + /// if the user is not logged on. + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string Transforms + { + get + { + // TODO: convert to IList? + return this["Transforms"]; + } + } + + /// + /// Gets the date and time when the patch is applied to the product. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public DateTime InstallDate + { + get + { + try + { + return DateTime.ParseExact( + this["InstallDate"], "yyyyMMdd", CultureInfo.InvariantCulture); + } + catch (FormatException) + { + return DateTime.MinValue; + } + } + } + + /// + /// True patch is marked as possible to uninstall from the product. + /// + ///

+ /// Even if this property is true, the installer can still block the + /// uninstallation if this patch is required by another patch that + /// cannot be uninstalled. + ///

+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Uninstallable")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool Uninstallable + { + get + { + return this["Uninstallable"] == "1"; + } + } + + /// + /// Get the registered display name for the patch. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string DisplayName + { + get + { + return this["DisplayName"]; + } + } + + /// + /// Gets the registered support information URL for the patch. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public Uri MoreInfoUrl + { + get + { + string value = this["MoreInfoURL"]; + if (!string.IsNullOrWhiteSpace(value)) + { + try + { + return new Uri(value); + } + catch (UriFormatException) { } + } + + return null; + } + } + + /// + /// Gets information about a specific patch installation. + /// + /// The property being retrieved; see remarks for valid properties. + /// The property value, or an empty string if the property is not set for the patch. + /// An unknown patch or property was requested + /// The installer configuration data is corrupt + ///

+ /// Win32 MSI APIs: + /// MsiGetPatchInfo, + /// MsiGetPatchInfoEx + ///

+ public override string this[string propertyName] + { + get + { + StringBuilder buf = new StringBuilder(""); + uint bufSize = 0; + uint ret; + + if (this.Context == UserContexts.UserManaged || + this.Context == UserContexts.UserUnmanaged || + this.Context == UserContexts.Machine) + { + ret = NativeMethods.MsiGetPatchInfoEx( + this.PatchCode, + this.ProductCode, + this.UserSid, + this.Context, + propertyName, + buf, + ref bufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + buf.Capacity = (int) ++bufSize; + ret = NativeMethods.MsiGetPatchInfoEx( + this.PatchCode, + this.ProductCode, + this.UserSid, + this.Context, + propertyName, + buf, + ref bufSize); + } + } + else + { + ret = NativeMethods.MsiGetPatchInfo( + this.PatchCode, + propertyName, + buf, + ref bufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + buf.Capacity = (int) ++bufSize; + ret = NativeMethods.MsiGetPatchInfo( + this.PatchCode, + propertyName, + buf, + ref bufSize); + } + } + + if (ret != 0) + { + return null; + } + + return buf.ToString(); + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ProductInstallation.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ProductInstallation.cs new file mode 100644 index 000000000..16f5a5abd --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ProductInstallation.cs @@ -0,0 +1,830 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Text; + + /// + /// Represents a unique instance of a product that + /// is either advertised, installed or unknown. + /// + internal class ProductInstallation : Installation + { + /// + /// Gets the set of all products with a specified upgrade code. This method lists the + /// currently installed and advertised products that have the specified UpgradeCode + /// property in their Property table. + /// + /// Upgrade code of related products + /// Enumeration of product codes + ///

+ /// Win32 MSI API: + /// MsiEnumRelatedProducts + ///

+ public static IEnumerable GetRelatedProducts(string upgradeCode) + { + StringBuilder buf = new StringBuilder(40); + for (uint i = 0; true; i++) + { + uint ret = NativeMethods.MsiEnumRelatedProducts(upgradeCode, 0, i, buf); + if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) break; + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + yield return new ProductInstallation(buf.ToString()); + } + } + + /// + /// Enumerates all product installations on the system. + /// + /// An enumeration of product objects. + ///

+ /// Win32 MSI API: + /// MsiEnumProducts, + ///

+ public static IEnumerable AllProducts + { + get + { + return GetProducts(null, null, UserContexts.All); + } + } + + /// + /// Enumerates product installations based on certain criteria. + /// + /// ProductCode (GUID) of the product instances to be enumerated. Only + /// instances of products within the scope of the context specified by the + /// and parameters will be + /// enumerated. This parameter may be set to null to enumerate all products in the specified + /// context. + /// Specifies a security identifier (SID) that restricts the context + /// of enumeration. A SID value other than s-1-1-0 is considered a user SID and restricts + /// enumeration to the current user or any user in the system. The special SID string + /// s-1-1-0 (Everyone) specifies enumeration across all users in the system. This parameter + /// can be set to null to restrict the enumeration scope to the current user. When + /// is set to the machine context only, + /// must be null. + /// Specifies the user context. + /// An enumeration of product objects for enumerated product instances. + ///

+ /// Win32 MSI API: + /// MsiEnumProductsEx + ///

+ public static IEnumerable GetProducts( + string productCode, string userSid, UserContexts context) + { + StringBuilder buf = new StringBuilder(40); + UserContexts targetContext; + StringBuilder targetSidBuf = new StringBuilder(40); + for (uint i = 0; ; i++) + { + uint targetSidBufSize = (uint) targetSidBuf.Capacity; + uint ret = NativeMethods.MsiEnumProductsEx( + productCode, + userSid, + context, + i, + buf, + out targetContext, + targetSidBuf, + ref targetSidBufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + targetSidBuf.Capacity = (int) ++targetSidBufSize; + ret = NativeMethods.MsiEnumProductsEx( + productCode, + userSid, + context, + i, + buf, + out targetContext, + targetSidBuf, + ref targetSidBufSize); + } + + if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) + { + break; + } + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + yield return new ProductInstallation( + buf.ToString(), + targetSidBuf.ToString(), + targetContext); + } + } + + private IDictionary properties; + + /// + /// Creates a new object for accessing information about a product installation on the current system. + /// + /// ProductCode (GUID) of the product. + ///

+ /// All available user contexts will be queried. + ///

+ public ProductInstallation(string productCode) + : this(productCode, null, UserContexts.All) + { + } + + /// + /// Creates a new object for accessing information about a product installation on the current system. + /// + /// ProductCode (GUID) of the product. + /// The specific user, when working in a user context. This + /// parameter may be null to indicate the current user. The parameter must be null + /// when working in a machine context. + /// The user context. The calling process must have administrative + /// privileges to get information for a product installed for a user other than the + /// current user. + public ProductInstallation(string productCode, string userSid, UserContexts context) + : base(productCode, userSid, context) + { + if (string.IsNullOrWhiteSpace(productCode)) + { + throw new ArgumentNullException("productCode"); + } + } + + internal ProductInstallation(IDictionary properties) + : base(properties["ProductCode"], null, UserContexts.None) + { + this.properties = properties; + } + + /// + /// Gets the set of published features for the product. + /// + /// Enumeration of published features for the product. + /// The installer configuration data is corrupt + ///

+ /// Because features are not ordered, any new feature has an arbitrary index, meaning + /// this property can return features in any order. + ///

+ /// Win32 MSI API: + /// MsiEnumFeatures + ///

+ public IEnumerable Features + { + get + { + StringBuilder buf = new StringBuilder(256); + for (uint i = 0; ; i++) + { + uint ret = NativeMethods.MsiEnumFeatures(this.ProductCode, i, buf, null); + + if (ret != 0) + { + break; + } + + yield return new FeatureInstallation(buf.ToString(), this.ProductCode); + } + } + } + + /// + /// Gets the ProductCode (GUID) of the product. + /// + public string ProductCode + { + get { return this.InstallationCode; } + } + + /// + /// Gets a value indicating whether this product is installed on the current system. + /// + public override bool IsInstalled + { + get + { + return (this.State == InstallState.Default); + } + } + + /// + /// Gets a value indicating whether this product is advertised on the current system. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool IsAdvertised + { + get + { + return (this.State == InstallState.Advertised); + } + } + + /// + /// Checks whether the product is installed with elevated privileges. An application is called + /// a "managed application" if elevated (system) privileges are used to install the application. + /// + /// True if the product is elevated; false otherwise + ///

+ /// Note that this property does not take into account policies such as AlwaysInstallElevated, + /// but verifies that the local system owns the product's registry data. + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool IsElevated + { + get + { + bool isElevated; + uint ret = NativeMethods.MsiIsProductElevated(this.ProductCode, out isElevated); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return isElevated; + } + } + + /// + /// Gets the source list of this product installation. + /// + internal override SourceList SourceList + { + get + { + return this.properties == null ? base.SourceList : null; + } + } + + internal InstallState State + { + get + { + if (this.properties != null) + { + return InstallState.Unknown; + } + else + { + int installState = NativeMethods.MsiQueryProductState(this.ProductCode); + return (InstallState) installState; + } + } + } + + internal override int InstallationType + { + get + { + const int MSICODE_PRODUCT = 0x00000000; + return MSICODE_PRODUCT; + } + } + + /// + /// The support link. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string HelpLink + { + get + { + return this["HelpLink"]; + } + } + + /// + /// The support telephone. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string HelpTelephone + { + get + { + return this["HelpTelephone"]; + } + } + + /// + /// Date and time the product was installed. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public DateTime InstallDate + { + get + { + try + { + return DateTime.ParseExact( + this["InstallDate"], "yyyyMMdd", CultureInfo.InvariantCulture); + } + catch (FormatException) + { + return DateTime.MinValue; + } + } + } + + /// + /// The installed product name. + /// + public string ProductName + { + get + { + return this["InstalledProductName"]; + } + } + + /// + /// The installation location. + /// + public string InstallLocation + { + get + { + return this["InstallLocation"]; + } + } + + /// + /// The installation source. + /// + public string InstallSource + { + get + { + return this["InstallSource"]; + } + } + + /// + /// The local cached package. + /// + public string LocalPackage + { + get + { + return this["LocalPackage"]; + } + } + + /// + /// The publisher. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string Publisher + { + get + { + return this["Publisher"]; + } + } + + /// + /// URL about information. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public Uri UrlInfoAbout + { + get + { + string value = this["URLInfoAbout"]; + if (!string.IsNullOrWhiteSpace(value)) + { + try + { + return new Uri(value); + } + catch (UriFormatException) { } + } + + return null; + } + } + + /// + /// The URL update information. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public Uri UrlUpdateInfo + { + get + { + string value = this["URLUpdateInfo"]; + if (!string.IsNullOrWhiteSpace(value)) + { + try + { + return new Uri(value); + } + catch (UriFormatException) { } + } + + return null; + } + } + + /// + /// The product version. + /// + public Version ProductVersion + { + get + { + string ver = this["VersionString"]; + return ProductInstallation.ParseVersion(ver); + } + } + + /// + /// The product identifier. + /// + ///

+ /// For more information, see + /// ProductID + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string ProductId + { + get + { + return this["ProductID"]; + } + } + + /// + /// The company that is registered to use the product. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string RegCompany + { + get + { + return this["RegCompany"]; + } + } + + /// + /// The owner who is registered to use the product. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string RegOwner + { + get + { + return this["RegOwner"]; + } + } + + /// + /// Transforms. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string AdvertisedTransforms + { + get + { + return this["Transforms"]; + } + } + + /// + /// Product language. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string AdvertisedLanguage + { + get + { + return this["Language"]; + } + } + + /// + /// Human readable product name. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string AdvertisedProductName + { + get + { + return this["ProductName"]; + } + } + + /// + /// True if the product is advertised per-machine; + /// false if it is per-user or not advertised. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool AdvertisedPerMachine + { + get + { + return this["AssignmentType"] == "1"; + } + } + + /// + /// Identifier of the package that a product is installed from. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string AdvertisedPackageCode + { + get + { + return this["PackageCode"]; + } + } + + /// + /// Version of the advertised product. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public Version AdvertisedVersion + { + get + { + string ver = this["Version"]; + return ProductInstallation.ParseVersion(ver); + } + } + + /// + /// Primary icon for the package. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string AdvertisedProductIcon + { + get + { + return this["ProductIcon"]; + } + } + + /// + /// Name of the installation package for the advertised product. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string AdvertisedPackageName + { + get + { + return this["PackageName"]; + } + } + + /// + /// True if the advertised product can be serviced by + /// non-administrators without elevation. + /// + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool PrivilegedPatchingAuthorized + { + get + { + return this["AuthorizedLUAApp"] == "1"; + } + } + + /// + /// Gets information about an installation of a product. + /// + /// Name of the property being retrieved. + /// An unknown product or property was requested + /// The installer configuration data is corrupt + ///

+ /// Win32 MSI APIs: + /// MsiGetProductInfo, + /// MsiGetProductInfoEx + ///

+ public override string this[string propertyName] + { + get + { + if (this.properties != null) + { + string value = null; + this.properties.TryGetValue(propertyName, out value); + return value; + } + else + { + StringBuilder buf = new StringBuilder(40); + uint bufSize = (uint) buf.Capacity; + uint ret; + + if (this.Context == UserContexts.UserManaged || + this.Context == UserContexts.UserUnmanaged || + this.Context == UserContexts.Machine) + { + ret = NativeMethods.MsiGetProductInfoEx( + this.ProductCode, + this.UserSid, + this.Context, + propertyName, + buf, + ref bufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + buf.Capacity = (int) ++bufSize; + ret = NativeMethods.MsiGetProductInfoEx( + this.ProductCode, + this.UserSid, + this.Context, + propertyName, + buf, + ref bufSize); + } + } + else + { + ret = NativeMethods.MsiGetProductInfo( + this.ProductCode, + propertyName, + buf, + ref bufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + buf.Capacity = (int) ++bufSize; + ret = NativeMethods.MsiGetProductInfo( + this.ProductCode, + propertyName, + buf, + ref bufSize); + } + } + + if (ret != 0) + { + return null; + } + + return buf.ToString(); + } + } + } + + /// + /// Gets the installed state for a product feature. + /// + /// The feature being queried; identifier from the + /// Feature table + /// Installation state of the feature for the product instance: either + /// , , + /// or . + ///

+ /// Win32 MSI APIs: + /// MsiQueryFeatureState, + /// MsiQueryFeatureStateEx + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public InstallState GetFeatureState(string feature) + { + if (this.properties != null) + { + return InstallState.Unknown; + } + else + { + int installState; + uint ret = NativeMethods.MsiQueryFeatureStateEx( + this.ProductCode, + this.UserSid, + this.Context, + feature, + out installState); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return (InstallState) installState; + } + } + + /// + /// Gets the installed state for a product component. + /// + /// The component being queried; GUID of the component + /// as found in the ComponentId column of the Component table. + /// Installation state of the component for the product instance: either + /// or . + ///

+ /// Win32 MSI API: + /// MsiQueryComponentState + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public InstallState GetComponentState(string component) + { + if (this.properties != null) + { + return InstallState.Unknown; + } + else + { + int installState; + uint ret = NativeMethods.MsiQueryComponentState( + this.ProductCode, + this.UserSid, + this.Context, + component, + out installState); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return (InstallState) installState; + } + } + + /// + /// Obtains and stores the user information and product ID from an installation wizard. + /// + ///

+ /// This method is typically called by an application during the first run of the application. The application + /// first gets the or . + /// If those properties are missing, the application calls CollectUserInfo. + /// CollectUserInfo opens the product's installation package and invokes a wizard sequence that collects + /// user information. Upon completion of the sequence, user information is registered. Since this API requires + /// an authored user interface, the user interface level should be set to full by calling + /// as . + ///

+ /// The CollectUserInfo method invokes a FirstRun dialog from the product installation database. + ///

+ /// Win32 MSI API: + /// MsiCollectUserInfo + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void CollectUserInfo() + { + if (this.properties == null) + { + uint ret = NativeMethods.MsiCollectUserInfo(this.InstallationCode); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + } + + /// + /// Some products might write some invalid/nonstandard version strings to the registry. + /// This method tries to get the best data it can. + /// + /// Version string retrieved from the registry. + /// Version object, or null if the version string is completely invalid. + private static Version ParseVersion(string ver) + { + if (ver != null) + { + int dotCount = 0; + for (int i = 0; i < ver.Length; i++) + { + char c = ver[i]; + if (c == '.') dotCount++; + else if (!Char.IsDigit(c)) + { + ver = ver.Substring(0, i); + break; + } + } + + if (ver.Length > 0) + { + if (dotCount == 0) + { + ver = ver + ".0"; + } + else if (dotCount > 3) + { + string[] verSplit = ver.Split('.'); + ver = String.Join(".", verSplit, 0, 4); + } + + try + { + return new Version(ver); + } + catch (ArgumentException) + { + } + } + } + + return null; + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Record.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Record.cs new file mode 100644 index 000000000..755846326 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Record.cs @@ -0,0 +1,1073 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Runtime.InteropServices; + using System.Text; + + /// + /// The Record object is a container for holding and transferring a variable number of values. + /// Fields within the record are numerically indexed and can contain strings, integers, streams, + /// and null values. Record fields are indexed starting with 1. Field 0 is a special format field. + /// + ///

+ /// Most methods on the Record class have overloads that allow using either a number + /// or a name to designate a field. However note that field names only exist when the + /// Record is directly returned from a query on a database. For other records, attempting + /// to access a field by name will result in an InvalidOperationException. + ///

+ internal class Record : InstallerHandle + { + private View view; + private bool isFormatStringInvalid; + + /// + /// IsFormatStringInvalid is set from several View methods that invalidate the FormatString + /// and used to determine behavior during Record.ToString(). + /// + internal protected bool IsFormatStringInvalid + { + set { this.isFormatStringInvalid = value; } + + get { return this.isFormatStringInvalid; } + } + + /// + /// Creates a new record object with the requested number of fields. + /// + /// Required number of fields, which may be 0. + /// The maximum number of fields in a record is limited to 65535. + ///

+ /// The Record object should be d after use. + /// It is best that the handle be closed manually as soon as it is no longer + /// needed, as leaving lots of unused handles open can degrade performance. + ///

+ /// Win32 MSI API: + /// MsiCreateRecord + ///

+ public Record(int fieldCount) + : this((IntPtr) RemotableNativeMethods.MsiCreateRecord((uint) fieldCount, 0), true, (View) null) + { + } + + /// + /// Creates a new record object, providing values for an arbitrary number of fields. + /// + /// The values of the record fields. The parameters should be of type Int16, Int32 or String + ///

+ /// The Record object should be d after use. + /// It is best that the handle be closed manually as soon as it is no longer + /// needed, as leaving lots of unused handles open can degrade performance. + ///

+ /// Win32 MSI API: + /// MsiCreateRecord + ///

+ public Record(params object[] fields) + : this(fields.Length) + { + if (fields== null) { + throw new ArgumentNullException("fields"); + } + + for (int i = 0; i < fields.Length; i++) + { + this[i + 1] = fields[i]; + } + } + + internal Record(IntPtr handle, bool ownsHandle, View view) + : base(handle, ownsHandle) + { + this.view = view; + } + + /// + /// Gets the number of fields in a record. + /// + ///

+ /// Win32 MSI API: + /// MsiRecordGetFieldCount + ///

+ public int FieldCount + { + get + { + return (int) RemotableNativeMethods.MsiRecordGetFieldCount((int) this.Handle); + } + } + + /// + /// Gets or sets field 0 of the Record, which is the format string. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string FormatString + { + get { return this.GetString(0); } + set { this.SetString(0, value); } + } + + /// + /// Gets or sets a record field value. + /// + /// Specifies the name of the field of the Record to get or set. + /// The name does not match any known field of the Record. + ///

+ /// When getting a field, the type of the object returned depends on the type of the Record field. + /// The object will be one of: Int16, Int32, String, Stream, or null. + ///

+ /// When setting a field, the type of the object provided will be converted to match the View + /// query that returned the record, or if Record was not returned from a view then the type of + /// the object provided will determine the type of the Record field. The object should be one of: + /// Int16, Int32, String, Stream, or null. + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public object this[string fieldName] + { + get + { + int field = this.FindColumn(fieldName); + return this[field]; + } + + set + { + int field = this.FindColumn(fieldName); + this[field] = value; + } + } + + /// + /// Gets or sets a record field value. + /// + /// Specifies the field of the Record to get or set. + /// The field is less than 0 or greater than the + /// number of fields in the Record. + ///

+ /// Record fields are indexed starting with 1. Field 0 is a special format field. + ///

+ /// When getting a field, the type of the object returned depends on the type of the Record field. + /// The object will be one of: Int16, Int32, String, Stream, or null. If the Record was returned + /// from a View, the type will match that of the field from the View query. Otherwise, the type + /// will match the type of the last value set for the field. + ///

+ /// When setting a field, the type of the object provided will be converted to match the View + /// query that returned the Record, or if Record was not returned from a View then the type of + /// the object provided will determine the type of the Record field. The object should be one of: + /// Int16, Int32, String, Stream, or null. + ///

+ /// The type-specific getters and setters are slightly more efficient than this property, since + /// they don't have to do the extra work to infer the value's type every time. + ///

+ /// Win32 MSI APIs: + /// MsiRecordGetInteger, + /// MsiRecordGetString, + /// MsiRecordSetInteger, + /// MsiRecordSetString + ///

+ public object this[int field] + { + get + { + if (field <= 0) + { + return this.GetString(0); + } + + Type valueType = null; + if (this.view != null) + { + this.CheckRange(field); + + valueType = this.view.Columns[field - 1].Type; + } + + if (valueType == null || valueType == typeof(String)) + { + return this.GetString(field); + } + else if (valueType == typeof(Stream)) + { + return this.IsNull(field) ? null : new RecordStream(this, field); + } + else + { + int? value = this.GetNullableInteger(field); + return value.HasValue ? (object) value.Value : null; + } + + } + + set + { + if (field == 0) + { + if (value == null) + { + value = String.Empty; + } + + this.SetString(0, value.ToString()); + } + else if (value == null) + { + this.SetNullableInteger(field, null); + } + else + { + Type valueType = value.GetType(); + if (valueType == typeof(Int32) || valueType == typeof(Int16)) + { + this.SetInteger(field, (int) value); + } + else if (valueType.IsSubclassOf(typeof(Stream))) + { + this.SetStream(field, (Stream) value); + } + else + { + this.SetString(field, value.ToString()); + } + } + } + } + + /// + /// Creates a new Record object from an integer record handle. + /// + ///

+ /// This method is only provided for interop purposes. A Record object + /// should normally be obtained by calling + /// other methods. + ///

The handle will be closed when this object is disposed or finalized.

+ ///

+ /// Integer record handle + /// true to close the handle when this object is disposed or finalized + public static Record FromHandle(IntPtr handle, bool ownsHandle) + { + return new Record(handle, ownsHandle, (View) null); + } + + /// + /// Sets all fields in a record to null. + /// + ///

+ /// Win32 MSI API: + /// MsiRecordClearData + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void Clear() + { + uint ret = RemotableNativeMethods.MsiRecordClearData((int) this.Handle); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Reports whether a record field is null. + /// + /// Specifies the field to check. + /// True if the field is null, false otherwise. + /// The field is less than 0 or greater than the + /// number of fields in the Record. + ///

+ /// Win32 MSI API: + /// MsiRecordIsNull + ///

+ public bool IsNull(int field) + { + this.CheckRange(field); + return RemotableNativeMethods.MsiRecordIsNull((int) this.Handle, (uint) field); + } + + /// + /// Reports whether a record field is null. + /// + /// Specifies the field to check. + /// True if the field is null, false otherwise. + /// The field name does not match any + /// of the named fields in the Record. + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool IsNull(string fieldName) + { + int field = this.FindColumn(fieldName); + return this.IsNull(field); + } + + /// + /// Gets the length of a record field. The count does not include the terminating null. + /// + /// The field is less than 0 or greater than the + /// number of fields in the Record. + ///

+ /// The returned data size is 0 if the field is null, non-existent, + /// or an internal object pointer. The method also returns 0 if the handle is not a valid + /// Record handle. + ///

+ /// If the data is in integer format, the property returns 2 or 4. + ///

+ /// If the data is in string format, the property returns the character count + /// (not including the NULL terminator). + ///

+ /// If the data is in stream format, the property returns the byte count. + ///

+ /// Win32 MSI API: + /// MsiRecordDataSize + ///

+ public int GetDataSize(int field) + { + this.CheckRange(field); + return (int) RemotableNativeMethods.MsiRecordDataSize((int) this.Handle, (uint) field); + } + + /// + /// Gets the length of a record field. The count does not include the terminating null. + /// + /// Specifies the field to check. + /// The field name does not match any + /// of the named fields in the Record. + ///

The returned data size is 0 if the field is null, non-existent, + /// or an internal object pointer. The method also returns 0 if the handle is not a valid + /// Record handle. + ///

+ /// If the data is in integer format, the property returns 2 or 4. + ///

+ /// If the data is in string format, the property returns the character count + /// (not including the NULL terminator). + ///

+ /// If the data is in stream format, the property returns the byte count. + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public int GetDataSize(string fieldName) + { + int field = this.FindColumn(fieldName); + return this.GetDataSize(field); + } + + /// + /// Gets a field value as an integer. + /// + /// Specifies the field to retrieve. + /// Integer value of the field, or 0 if the field is null. + /// The field is less than 0 or greater than the + /// number of fields in the Record. + ///

+ /// Win32 MSI API: + /// MsiRecordGetInteger + ///

+ /// + [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "integer")] + public int GetInteger(int field) + { + this.CheckRange(field); + + int value = RemotableNativeMethods.MsiRecordGetInteger((int) this.Handle, (uint) field); + if (value == Int32.MinValue) // MSI_NULL_INTEGER + { + return 0; + } + return value; + } + + /// + /// Gets a field value as an integer. + /// + /// Specifies the field to retrieve. + /// Integer value of the field, or 0 if the field is null. + /// The field name does not match any + /// of the named fields in the Record. + /// + [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "integer")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public int GetInteger(string fieldName) + { + int field = this.FindColumn(fieldName); + return this.GetInteger(field); + } + + /// + /// Gets a field value as an integer. + /// + /// Specifies the field to retrieve. + /// Integer value of the field, or null if the field is null. + /// The field is less than 0 or greater than the + /// number of fields in the Record. + ///

+ /// Win32 MSI API: + /// MsiRecordGetInteger + ///

+ /// + public int? GetNullableInteger(int field) + { + this.CheckRange(field); + + int value = RemotableNativeMethods.MsiRecordGetInteger((int) this.Handle, (uint) field); + if (value == Int32.MinValue) // MSI_NULL_INTEGER + { + return null; + } + return value; + } + + /// + /// Gets a field value as an integer. + /// + /// Specifies the field to retrieve. + /// Integer value of the field, or null if the field is null. + /// The field name does not match any + /// of the named fields in the Record. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public int? GetNullableInteger(string fieldName) + { + int field = this.FindColumn(fieldName); + return this.GetInteger(field); + } + + /// + /// Sets the value of a field to an integer. + /// + /// Specifies the field to set. + /// new value of the field + /// The field is less than 0 or greater than the + /// number of fields in the Record. + ///

+ /// Win32 MSI API: + /// MsiRecordSetInteger + ///

+ /// + public void SetInteger(int field, int value) + { + this.CheckRange(field); + + uint ret = RemotableNativeMethods.MsiRecordSetInteger((int) this.Handle, (uint) field, value); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Sets the value of a field to an integer. + /// + /// Specifies the field to set. + /// new value of the field + /// The field name does not match any + /// of the named fields in the Record. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void SetInteger(string fieldName, int value) + { + int field = this.FindColumn(fieldName); + this.SetInteger(field, value); + } + + /// + /// Sets the value of a field to a nullable integer. + /// + /// Specifies the field to set. + /// new value of the field + /// The field is less than 0 or greater than the + /// number of fields in the Record. + ///

+ /// Win32 MSI API: + /// MsiRecordSetInteger + ///

+ /// + public void SetNullableInteger(int field, int? value) + { + this.CheckRange(field); + + uint ret = RemotableNativeMethods.MsiRecordSetInteger( + (int) this.Handle, + (uint) field, + value.HasValue ? (int) value : Int32.MinValue); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Sets the value of a field to a nullable integer. + /// + /// Specifies the field to set. + /// new value of the field + /// The field name does not match any + /// of the named fields in the Record. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void SetNullableInteger(string fieldName, int? value) + { + int field = this.FindColumn(fieldName); + this.SetNullableInteger(field, value); + } + + /// + /// Gets a field value as a string. + /// + /// Specifies the field to retrieve. + /// String value of the field, or an empty string if the field is null. + /// The field is less than 0 or greater than the + /// number of fields in the Record. + ///

+ /// Win32 MSI API: + /// MsiRecordGetString + ///

+ public string GetString(int field) + { + this.CheckRange(field); + + StringBuilder buf = new StringBuilder(String.Empty); + uint bufSize = 0; + uint ret = RemotableNativeMethods.MsiRecordGetString((int) this.Handle, (uint) field, buf, ref bufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + buf.Capacity = (int) ++bufSize; + ret = RemotableNativeMethods.MsiRecordGetString((int) this.Handle, (uint) field, buf, ref bufSize); + } + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return buf.ToString(); + } + + /// + /// Gets a field value as a string. + /// + /// Specifies the field to retrieve. + /// String value of the field, or an empty string if the field is null. + /// The field name does not match any + /// of the named fields in the Record. + public string GetString(string fieldName) + { + int field = this.FindColumn(fieldName); + return this.GetString(field); + } + + /// + /// Sets the value of a field to a string. + /// + /// Specifies the field to set. + /// new value of the field + /// The field is less than 0 or greater than the + /// number of fields in the Record. + ///

+ /// Win32 MSI API: + /// MsiRecordSetString + ///

+ public void SetString(int field, string value) + { + this.CheckRange(field); + + if (value == null) + { + value = String.Empty; + } + + uint ret = RemotableNativeMethods.MsiRecordSetString((int) this.Handle, (uint) field, value); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + // If we set the FormatString manually, then it should be valid again + if (field == 0) + { + this.IsFormatStringInvalid = false; + } + } + + /// + /// Sets the value of a field to a string. + /// + /// Specifies the field to set. + /// new value of the field + /// The field name does not match any + /// of the named fields in the Record. + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void SetString(string fieldName, string value) + { + int field = this.FindColumn(fieldName); + this.SetString(field, value); + } + + /// + /// Reads a record stream field into a file. + /// + /// Specifies the field of the Record to get. + /// Specifies the path to the file to contain the stream. + /// The field is less than 0 or greater than the + /// number of fields in the Record. + /// Attempt to extract a storage from a database open + /// in read-write mode, or from a database without an associated file path + ///

+ /// This method is capable of directly extracting substorages. To do so, first select both the + /// `Name` and `Data` column of the `_Storages` table, then get the stream of the `Data` field. + /// However, substorages may only be extracted from a database that is open in read-only mode. + ///

+ /// Win32 MSI API: + /// MsiRecordReadStream + ///

+ public void GetStream(int field, string filePath) + { + if (string.IsNullOrWhiteSpace(filePath)) + { + throw new ArgumentNullException("filePath"); + } + + IList tables = (this.view != null ? this.view.Tables : null); + if (tables != null && tables.Count == 1 && tables[0].Name == "_Storages" && field == this.FindColumn("Data")) + { + if (!this.view.Database.IsReadOnly) + { + throw new NotSupportedException("Database must be opened read-only to support substorage extraction."); + } + else if (this.view.Database.FilePath == null) + { + throw new NotSupportedException("Database must have an associated file path to support substorage extraction."); + } + else if (this.FindColumn("Name") <= 0) + { + throw new NotSupportedException("Name column must be part of the Record in order to extract substorage."); + } + else + { + Record.ExtractSubStorage(this.view.Database.FilePath, this.GetString("Name"), filePath); + } + } + else + { + if (!this.IsNull(field)) + { + Stream readStream = null, writeStream = null; + try + { + readStream = new RecordStream(this, field); + writeStream = new FileStream(filePath, FileMode.Create, FileAccess.Write); + int count = 512; + byte[] buf = new byte[count]; + while (count == buf.Length) + { + if ((count = readStream.Read(buf, 0, buf.Length)) > 0) + { + writeStream.Write(buf, 0, count); + } + } + } + finally + { + if (readStream != null) readStream.Close(); + if (writeStream != null) writeStream.Close(); + } + } + } + } + + /// + /// Reads a record stream field into a file. + /// + /// Specifies the field of the Record to get. + /// Specifies the path to the file to contain the stream. + /// The field name does not match any + /// of the named fields in the Record. + /// Attempt to extract a storage from a database open + /// in read-write mode, or from a database without an associated file path + ///

+ /// This method is capable of directly extracting substorages. To do so, first select both the + /// `Name` and `Data` column of the `_Storages` table, then get the stream of the `Data` field. + /// However, substorages may only be extracted from a database that is open in read-only mode. + ///

+ public void GetStream(string fieldName, string filePath) + { + int field = this.FindColumn(fieldName); + this.GetStream(field, filePath); + } + + /// + /// Gets a record stream field. + /// + /// Specifies the field of the Record to get. + /// A Stream that reads the field data. + /// The field is less than 0 or greater than the + /// number of fields in the Record. + ///

+ /// This method is not capable of reading substorages. To extract a substorage, + /// use . + ///

+ /// Win32 MSI API: + /// MsiRecordReadStream + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public Stream GetStream(int field) + { + this.CheckRange(field); + + return this.IsNull(field) ? null : new RecordStream(this, field); + } + + /// + /// Gets a record stream field. + /// + /// Specifies the field of the Record to get. + /// A Stream that reads the field data. + /// The field name does not match any + /// of the named fields in the Record. + ///

+ /// This method is not capable of reading substorages. To extract a substorage, + /// use . + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public Stream GetStream(string fieldName) + { + int field = this.FindColumn(fieldName); + return this.GetStream(field); + } + + /// + /// Sets a record stream field from a file. Stream data cannot be inserted into temporary fields. + /// + /// Specifies the field of the Record to set. + /// Specifies the path to the file containing the stream. + /// The field is less than 0 or greater than the + /// number of fields in the Record. + ///

+ /// The contents of the specified file are read into a stream object. The stream persists if + /// the Record is inserted into the Database and the Database is committed. + ///

+ /// To reset the stream to its beginning you must pass in null for filePath. + /// Do not pass an empty string, "", to reset the stream. + ///

+ /// Setting a stream with this method is more efficient than setting a field to a + /// FileStream object. + ///

+ /// Win32 MSI API: + /// MsiRecordsetStream + ///

+ public void SetStream(int field, string filePath) + { + this.CheckRange(field); + + if (string.IsNullOrWhiteSpace(filePath)) + { + throw new ArgumentNullException("filePath"); + } + + uint ret = RemotableNativeMethods.MsiRecordSetStream((int) this.Handle, (uint) field, filePath); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Sets a record stream field from a file. Stream data cannot be inserted into temporary fields. + /// + /// Specifies the field name of the Record to set. + /// Specifies the path to the file containing the stream. + /// The field name does not match any + /// of the named fields in the Record. + ///

+ /// The contents of the specified file are read into a stream object. The stream persists if + /// the Record is inserted into the Database and the Database is committed. + /// To reset the stream to its beginning you must pass in null for filePath. + /// Do not pass an empty string, "", to reset the stream. + ///

+ /// Setting a stream with this method is more efficient than setting a field to a + /// FileStream object. + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void SetStream(string fieldName, string filePath) + { + int field = this.FindColumn(fieldName); + this.SetStream(field, filePath); + } + + /// + /// Sets a record stream field from a Stream object. Stream data cannot be inserted into temporary fields. + /// + /// Specifies the field of the Record to set. + /// Specifies the stream data. + /// The field is less than 0 or greater than the + /// number of fields in the Record. + ///

+ /// The stream persists if the Record is inserted into the Database and the Database is committed. + ///

+ /// Win32 MSI API: + /// MsiRecordsetStream + ///

+ public void SetStream(int field, Stream stream) + { + this.CheckRange(field); + + if (stream == null) + { + uint ret = RemotableNativeMethods.MsiRecordSetStream((int) this.Handle, (uint) field, null); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + else + { + Stream writeStream = null; + string tempPath = Path.GetTempFileName(); + try + { + writeStream = new FileStream(tempPath, FileMode.Truncate, FileAccess.Write); + byte[] buf = new byte[512]; + int count; + while ((count = stream.Read(buf, 0, buf.Length)) > 0) + { + writeStream.Write(buf, 0, count); + } + writeStream.Close(); + writeStream = null; + + uint ret = RemotableNativeMethods.MsiRecordSetStream((int) this.Handle, (uint) field, tempPath); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + finally + { + if (writeStream != null) writeStream.Close(); + if (File.Exists(tempPath)) + { + try + { + File.Delete(tempPath); + } + catch (IOException) + { + if (this.view != null) + { + this.view.Database.DeleteOnClose(tempPath); + } + } + } + } + } + } + + /// + /// Sets a record stream field from a Stream object. Stream data cannot be inserted into temporary fields. + /// + /// Specifies the field name of the Record to set. + /// Specifies the stream data. + /// The field name does not match any + /// of the named fields in the Record. + ///

+ /// The stream persists if the Record is inserted into the Database and the Database is committed. + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void SetStream(string fieldName, Stream stream) + { + int field = this.FindColumn(fieldName); + this.SetStream(field, stream); + } + + /// + /// Gets a formatted string representation of the Record. + /// + /// A formatted string representation of the Record. + ///

+ /// If field 0 of the Record is set to a nonempty string, it is used to format the data in the Record. + ///

+ /// Win32 MSI API: + /// MsiFormatRecord + ///

+ /// + /// + public override string ToString() + { + return this.ToString((IFormatProvider) null); + } + + /// + /// Gets a formatted string representation of the Record, optionally using a Session to format properties. + /// + /// an optional Session instance that will be used to lookup any + /// properties in the Record's format string + /// A formatted string representation of the Record. + ///

+ /// If field 0 of the Record is set to a nonempty string, it is used to format the data in the Record. + ///

+ /// Win32 MSI API: + /// MsiFormatRecord + ///

+ /// + /// + public string ToString(IFormatProvider provider) + { + if (this.IsFormatStringInvalid) // Format string is invalid + { + // TODO: return all values by default? + return String.Empty; + } + + InstallerHandle session = provider as InstallerHandle; + int sessionHandle = session != null ? (int) session.Handle : 0; + StringBuilder buf = new StringBuilder(String.Empty); + uint bufSize = 1; + uint ret = RemotableNativeMethods.MsiFormatRecord(sessionHandle, (int) this.Handle, buf, ref bufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + bufSize++; + buf = new StringBuilder((int) bufSize); + ret = RemotableNativeMethods.MsiFormatRecord(sessionHandle, (int) this.Handle, buf, ref bufSize); + } + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return buf.ToString(); + } + + /// + /// Gets a formatted string representation of the Record. + /// + /// String to be used to format the data in the Record, + /// instead of the Record's format string. + /// A formatted string representation of the Record. + ///

+ /// Win32 MSI API: + /// MsiFormatRecord + ///

+ [Obsolete("This method is obsolete because it has undesirable side-effects. As an alternative, set the FormatString " + + "property separately before calling the ToString() override that takes no parameters.")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string ToString(string format) + { + return this.ToString(format, null); + } + + /// + /// Gets a formatted string representation of the Record, optionally using a Session to format properties. + /// + /// String to be used to format the data in the Record, + /// instead of the Record's format string. + /// an optional Session instance that will be used to lookup any + /// properties in the Record's format string + /// A formatted string representation of the Record. + ///

+ /// Win32 MSI API: + /// MsiFormatRecord + ///

+ /// + /// + [Obsolete("This method is obsolete because it has undesirable side-effects. As an alternative, set the FormatString " + + "property separately before calling the ToString() override that takes just a format provider.")] + public string ToString(string format, IFormatProvider provider) + { + if (format == null) + { + return this.ToString(provider); + } + else if (format.Length == 0) + { + return String.Empty; + } + else + { + string savedFormatString = (string) this[0]; + try + { + this.FormatString = format; + return this.ToString(provider); + } + finally + { + this.FormatString = savedFormatString; + } + } + } + + [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")] + private static void ExtractSubStorage(string databaseFile, string storageName, string extractFile) + { + IStorage storage; + NativeMethods.STGM openMode = NativeMethods.STGM.READ | NativeMethods.STGM.SHARE_DENY_WRITE; + int hr = NativeMethods.StgOpenStorage(databaseFile, IntPtr.Zero, (uint) openMode, IntPtr.Zero, 0, out storage); + if (hr != 0) + { + Marshal.ThrowExceptionForHR(hr); + } + + try + { + openMode = NativeMethods.STGM.READ | NativeMethods.STGM.SHARE_EXCLUSIVE; + IStorage subStorage = storage.OpenStorage(storageName, IntPtr.Zero, (uint) openMode, IntPtr.Zero, 0); + + try + { + IStorage newStorage; + openMode = NativeMethods.STGM.CREATE | NativeMethods.STGM.READWRITE | NativeMethods.STGM.SHARE_EXCLUSIVE; + hr = NativeMethods.StgCreateDocfile(extractFile, (uint) openMode, 0, out newStorage); + if (hr != 0) + { + Marshal.ThrowExceptionForHR(hr); + } + + try + { + subStorage.CopyTo(0, IntPtr.Zero, IntPtr.Zero, newStorage); + + newStorage.Commit(0); + } + finally + { + Marshal.ReleaseComObject(newStorage); + } + } + finally + { + Marshal.ReleaseComObject(subStorage); + } + } + finally + { + Marshal.ReleaseComObject(storage); + } + } + + private int FindColumn(string fieldName) + { + if (this.view == null) + { + throw new InvalidOperationException(); + } + ColumnCollection columns = this.view.Columns; + for (int i = 0; i < columns.Count; i++) + { + if (columns[i].Name == fieldName) + { + return i + 1; + } + } + throw new ArgumentOutOfRangeException("fieldName"); + } + + private void CheckRange(int field) + { + if (field < 0 || field > this.FieldCount) + { + throw new ArgumentOutOfRangeException("field"); + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/RecordStream.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/RecordStream.cs new file mode 100644 index 000000000..0df35e7d0 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/RecordStream.cs @@ -0,0 +1,99 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.IO; + + internal class RecordStream : Stream + { + private Record record; + private int field; + private long position; + + internal RecordStream(Record record, int field) + : base() + { + this.record = record; + this.field = field; + } + + public override bool CanRead { get { return true; } } + public override bool CanWrite { get { return false; } } + public override bool CanSeek { get { return false; } } + + public override long Length + { + get + { + return this.record.GetDataSize(this.field); + } + } + + public override long Position + { + get + { + return this.position; + } + + set + { + throw new NotSupportedException(); + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (count > 0) + { + byte[] readBuffer = (offset == 0 ? buffer : new byte[count]); + uint ucount = (uint) count; + uint ret = RemotableNativeMethods.MsiRecordReadStream((int) this.record.Handle, (uint) this.field, buffer, ref ucount); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + count = (int) ucount; + if (offset > 0) + { + Array.Copy(readBuffer, 0, buffer, offset, count); + } + this.position += count; + } + return count; + } + + public override void Write(byte[] array, int offset, int count) + { + throw new NotSupportedException(); + } + + public override string ToString() + { + return "[Binary data]"; + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/RemotableNativeMethods.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/RemotableNativeMethods.cs new file mode 100644 index 000000000..eec8c527b --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/RemotableNativeMethods.cs @@ -0,0 +1,1189 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Runtime.InteropServices; + using System.Text; + + /// + /// Assigns ID numbers to the MSI APIs that are remotable. + /// + ///

+ /// This enumeration MUST stay in sync with the + /// unmanaged equivalent in RemoteMsiSession.h! + ///

+ internal enum RemoteMsiFunctionId + { + EndSession = 0, + MsiCloseHandle, + MsiCreateRecord, + MsiDatabaseGetPrimaryKeys, + MsiDatabaseIsTablePersistent, + MsiDatabaseOpenView, + MsiDoAction, + MsiEnumComponentCosts, + MsiEvaluateCondition, + MsiFormatRecord, + MsiGetActiveDatabase, + MsiGetComponentState, + MsiGetFeatureCost, + MsiGetFeatureState, + MsiGetFeatureValidStates, + MsiGetLanguage, + MsiGetLastErrorRecord, + MsiGetMode, + MsiGetProperty, + MsiGetSourcePath, + MsiGetSummaryInformation, + MsiGetTargetPath, + MsiProcessMessage, + MsiRecordClearData, + MsiRecordDataSize, + MsiRecordGetFieldCount, + MsiRecordGetInteger, + MsiRecordGetString, + MsiRecordIsNull, + MsiRecordReadStream, + MsiRecordSetInteger, + MsiRecordSetStream, + MsiRecordSetString, + MsiSequence, + MsiSetComponentState, + MsiSetFeatureAttributes, + MsiSetFeatureState, + MsiSetInstallLevel, + MsiSetMode, + MsiSetProperty, + MsiSetTargetPath, + MsiSummaryInfoGetProperty, + MsiVerifyDiskSpace, + MsiViewExecute, + MsiViewFetch, + MsiViewGetError, + MsiViewGetColumnInfo, + MsiViewModify, + } + + /// + /// Defines the signature of the native function + /// in SfxCA.dll that implements the remoting call. + /// + internal delegate void MsiRemoteInvoke( + RemoteMsiFunctionId id, + [MarshalAs(UnmanagedType.SysInt)] + IntPtr request, + [MarshalAs(UnmanagedType.SysInt)] + out IntPtr response); + + /// + /// Redirects native API calls to either the normal NativeMethods class + /// or to out-of-proc calls via the remoting channel. + /// + internal static class RemotableNativeMethods + { + private const int MAX_REQUEST_FIELDS = 4; + private static int requestFieldDataOffset; + private static int requestFieldSize; + + [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] + private static IntPtr requestBuf; + + private static MsiRemoteInvoke remotingDelegate; + + /// + /// Checks if the current process is using remoting to access the + /// MSI session and database APIs. + /// + internal static bool RemotingEnabled + { + get + { + return RemotableNativeMethods.remotingDelegate != null; + } + } + + /// + /// Sets a delegate that is used to make remote API calls. + /// + ///

+ /// The implementation of this delegate is provided by the + /// custom action host DLL. + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal static MsiRemoteInvoke RemotingDelegate + { + set + { + RemotableNativeMethods.remotingDelegate = value; + + if (value != null && requestBuf == IntPtr.Zero) + { + requestFieldDataOffset = Marshal.SizeOf(typeof(IntPtr)); + requestFieldSize = 2 * Marshal.SizeOf(typeof(IntPtr)); + RemotableNativeMethods.requestBuf = Marshal.AllocHGlobal( + requestFieldSize * MAX_REQUEST_FIELDS); + } + } + } + + internal static bool IsRemoteHandle(int handle) + { + return (handle & Int32.MinValue) != 0; + } + + internal static int MakeRemoteHandle(int handle) + { + if (handle == 0) + { + return handle; + } + + if (RemotableNativeMethods.IsRemoteHandle(handle)) + { + throw new InvalidOperationException("Handle already has the remote bit set."); + } + + return handle ^ Int32.MinValue; + } + + internal static int GetRemoteHandle(int handle) + { + if (handle == 0) + { + return handle; + } + + if (!RemotableNativeMethods.IsRemoteHandle(handle)) + { + throw new InvalidOperationException("Handle does not have the remote bit set."); + } + + return handle ^ Int32.MinValue; + } + + private static void ClearData(IntPtr buf) + { + for (int i = 0; i < MAX_REQUEST_FIELDS; i++) + { + Marshal.WriteInt32(buf, (i * requestFieldSize), (int) VarEnum.VT_NULL); + Marshal.WriteIntPtr(buf, (i * requestFieldSize) + requestFieldDataOffset, IntPtr.Zero); + } + } + + private static void WriteInt(IntPtr buf, int field, int value) + { + Marshal.WriteInt32(buf, (field * requestFieldSize), (int) VarEnum.VT_I4); + Marshal.WriteInt32(buf, (field * requestFieldSize) + requestFieldDataOffset, value); + } + + private static void WriteString(IntPtr buf, int field, string value) + { + if (value == null) + { + Marshal.WriteInt32(buf, (field * requestFieldSize), (int) VarEnum.VT_NULL); + Marshal.WriteIntPtr(buf, (field * requestFieldSize) + requestFieldDataOffset, IntPtr.Zero); + } + else + { + IntPtr stringPtr = Marshal.StringToHGlobalUni(value); + Marshal.WriteInt32(buf, (field * requestFieldSize), (int) VarEnum.VT_LPWSTR); + Marshal.WriteIntPtr(buf, (field * requestFieldSize) + requestFieldDataOffset, stringPtr); + } + } + + private static int ReadInt(IntPtr buf, int field) + { + VarEnum vt = (VarEnum) Marshal.ReadInt32(buf, (field * requestFieldSize)); + if (vt == VarEnum.VT_EMPTY) + { + return 0; + } + else if (vt != VarEnum.VT_I4 && vt != VarEnum.VT_UI4) + { + throw new InstallerException("Invalid data received from remote MSI function invocation."); + } + return Marshal.ReadInt32(buf, (field * requestFieldSize) + requestFieldDataOffset); + } + + private static void ReadString(IntPtr buf, int field, StringBuilder szBuf, ref uint cchBuf) + { + VarEnum vt = (VarEnum) Marshal.ReadInt32(buf, (field * requestFieldSize)); + if (vt == VarEnum.VT_NULL) + { + szBuf.Remove(0, szBuf.Length); + return; + } + else if (vt != VarEnum.VT_LPWSTR) + { + throw new InstallerException("Invalid data received from remote MSI function invocation."); + } + + szBuf.Remove(0, szBuf.Length); + IntPtr strPtr = Marshal.ReadIntPtr(buf, (field * requestFieldSize) + requestFieldDataOffset); + string str = Marshal.PtrToStringUni(strPtr); + if (str != null) + { + szBuf.Append(str); + } + cchBuf = (uint) szBuf.Length; + } + + private static void FreeString(IntPtr buf, int field) + { + IntPtr stringPtr = Marshal.ReadIntPtr(buf, (field * requestFieldSize) + requestFieldDataOffset); + if (stringPtr != null) + { + Marshal.FreeHGlobal(stringPtr); + } + } + + private static void ReadStream(IntPtr buf, int field, byte[] sBuf, int count) + { + VarEnum vt = (VarEnum) Marshal.ReadInt32(buf, (field * requestFieldSize)); + if (vt != VarEnum.VT_STREAM) + { + throw new InstallerException("Invalid data received from remote MSI function invocation."); + } + + IntPtr sPtr = Marshal.ReadIntPtr(buf, (field * requestFieldSize) + requestFieldDataOffset); + Marshal.Copy(sPtr, sBuf, 0, count); + } + + private static uint MsiFunc_III(RemoteMsiFunctionId id, int in1, int in2, int in3) + { + lock (RemotableNativeMethods.remotingDelegate) + { + ClearData(requestBuf); + WriteInt(requestBuf, 0, in1); + WriteInt(requestBuf, 1, in2); + WriteInt(requestBuf, 2, in3); + IntPtr resp; + remotingDelegate(id, requestBuf, out resp); + return unchecked ((uint) ReadInt(resp, 0)); + } + } + + private static uint MsiFunc_IIS(RemoteMsiFunctionId id, int in1, int in2, string in3) + { + lock (RemotableNativeMethods.remotingDelegate) + { + ClearData(requestBuf); + WriteInt(requestBuf, 0, in1); + WriteInt(requestBuf, 1, in2); + WriteString(requestBuf, 2, in3); + IntPtr resp; + remotingDelegate(id, requestBuf, out resp); + FreeString(requestBuf, 2); + return unchecked ((uint) ReadInt(resp, 0)); + } + } + + private static uint MsiFunc_ISI(RemoteMsiFunctionId id, int in1, string in2, int in3) + { + lock (RemotableNativeMethods.remotingDelegate) + { + ClearData(requestBuf); + WriteInt(requestBuf, 0, in1); + WriteString(requestBuf, 1, in2); + WriteInt(requestBuf, 2, in3); + IntPtr resp; + remotingDelegate(id, requestBuf, out resp); + FreeString(requestBuf, 2); + return unchecked ((uint) ReadInt(resp, 0)); + } + } + + private static uint MsiFunc_ISS(RemoteMsiFunctionId id, int in1, string in2, string in3) + { + lock (RemotableNativeMethods.remotingDelegate) + { + ClearData(requestBuf); + WriteInt(requestBuf, 0, in1); + WriteString(requestBuf, 1, in2); + WriteString(requestBuf, 2, in3); + IntPtr resp; + remotingDelegate(id, requestBuf, out resp); + FreeString(requestBuf, 1); + FreeString(requestBuf, 2); + return unchecked ((uint) ReadInt(resp, 0)); + } + } + + private static uint MsiFunc_II_I(RemoteMsiFunctionId id, int in1, int in2, out int out1) + { + lock (RemotableNativeMethods.remotingDelegate) + { + ClearData(requestBuf); + WriteInt(requestBuf, 0, in1); + WriteInt(requestBuf, 1, in2); + IntPtr resp; + remotingDelegate(id, requestBuf, out resp); + uint ret = unchecked ((uint) ReadInt(resp, 0)); + out1 = ReadInt(resp, 1); + return ret; + } + } + + private static uint MsiFunc_ISII_I(RemoteMsiFunctionId id, int in1, string in2, int in3, int in4, out int out1) + { + lock (RemotableNativeMethods.remotingDelegate) + { + ClearData(requestBuf); + WriteInt(requestBuf, 0, in1); + WriteString(requestBuf, 1, in2); + WriteInt(requestBuf, 2, in3); + WriteInt(requestBuf, 3, in4); + IntPtr resp; + remotingDelegate(id, requestBuf, out resp); + FreeString(requestBuf, 1); + uint ret = unchecked ((uint) ReadInt(resp, 0)); + out1 = ReadInt(resp, 1); + return ret; + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + private static uint MsiFunc_IS_II(RemoteMsiFunctionId id, int in1, string in2, out int out1, out int out2) + { + lock (RemotableNativeMethods.remotingDelegate) + { + ClearData(requestBuf); + WriteInt(requestBuf, 0, in1); + WriteString(requestBuf, 1, in2); + IntPtr resp; + remotingDelegate(id, requestBuf, out resp); + FreeString(requestBuf, 1); + uint ret = unchecked ((uint) ReadInt(resp, 0)); + out1 = ReadInt(resp, 1); + out2 = ReadInt(resp, 2); + return ret; + } + } + + private static uint MsiFunc_II_S(RemoteMsiFunctionId id, int in1, int in2, StringBuilder out1, ref uint cchOut1) + { + lock (RemotableNativeMethods.remotingDelegate) + { + ClearData(requestBuf); + WriteInt(requestBuf, 0, in1); + WriteInt(requestBuf, 1, in2); + IntPtr resp; + remotingDelegate(id, requestBuf, out resp); + uint ret = unchecked ((uint) ReadInt(resp, 0)); + if (ret == 0) ReadString(resp, 1, out1, ref cchOut1); + return ret; + } + } + + private static uint MsiFunc_IS_S(RemoteMsiFunctionId id, int in1, string in2, StringBuilder out1, ref uint cchOut1) + { + lock (RemotableNativeMethods.remotingDelegate) + { + ClearData(requestBuf); + WriteInt(requestBuf, 0, in1); + WriteString(requestBuf, 1, in2); + IntPtr resp; + remotingDelegate(id, requestBuf, out resp); + FreeString(requestBuf, 1); + uint ret = unchecked ((uint) ReadInt(resp, 0)); + if (ret == 0) ReadString(resp, 1, out1, ref cchOut1); + return ret; + } + } + + private static uint MsiFunc_ISII_SII(RemoteMsiFunctionId id, int in1, string in2, int in3, int in4, StringBuilder out1, ref uint cchOut1, out int out2, out int out3) + { + lock (RemotableNativeMethods.remotingDelegate) + { + ClearData(requestBuf); + WriteInt(requestBuf, 0, in1); + WriteString(requestBuf, 1, in2); + WriteInt(requestBuf, 2, in3); + WriteInt(requestBuf, 3, in4); + IntPtr resp; + remotingDelegate(id, requestBuf, out resp); + FreeString(requestBuf, 1); + uint ret = unchecked ((uint) ReadInt(resp, 0)); + if (ret == 0) ReadString(resp, 1, out1, ref cchOut1); + out2 = ReadInt(resp, 2); + out3 = ReadInt(resp, 3); + return ret; + } + } + + internal static int MsiProcessMessage(int hInstall, uint eMessageType, int hRecord) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + { + return NativeMethods.MsiProcessMessage(hInstall, eMessageType, hRecord); + } + else lock (remotingDelegate) + { + // I don't understand why, but this particular function doesn't work + // when using the static requestBuf -- some data doesn't make it through. + // But it works when a fresh buffer is allocated here every call. + IntPtr buf = Marshal.AllocHGlobal( + requestFieldSize * MAX_REQUEST_FIELDS); + ClearData(buf); + WriteInt(buf, 0, RemotableNativeMethods.GetRemoteHandle(hInstall)); + WriteInt(buf, 1, unchecked ((int) eMessageType)); + WriteInt(buf, 2, RemotableNativeMethods.GetRemoteHandle(hRecord)); + IntPtr resp; + remotingDelegate(RemoteMsiFunctionId.MsiProcessMessage, buf, out resp); + Marshal.FreeHGlobal(buf); + return ReadInt(resp, 0); + } + } + + internal static uint MsiCloseHandle(int hAny) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hAny)) + return NativeMethods.MsiCloseHandle(hAny); + else + return RemotableNativeMethods.MsiFunc_III( + RemoteMsiFunctionId.MsiCloseHandle, RemotableNativeMethods.GetRemoteHandle(hAny), 0, 0); + } + + internal static uint MsiGetProperty(int hInstall, string szName, StringBuilder szValueBuf, ref uint cchValueBuf) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiGetProperty(hInstall, szName, szValueBuf, ref cchValueBuf); + else + { + return RemotableNativeMethods.MsiFunc_IS_S( + RemoteMsiFunctionId.MsiGetProperty, + RemotableNativeMethods.GetRemoteHandle(hInstall), + szName, + szValueBuf, + ref cchValueBuf); + } + } + + internal static uint MsiSetProperty(int hInstall, string szName, string szValue) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiSetProperty(hInstall, szName, szValue); + else + { + return RemotableNativeMethods.MsiFunc_ISS( + RemoteMsiFunctionId.MsiSetProperty, + RemotableNativeMethods.GetRemoteHandle(hInstall), + szName, + szValue); + } + } + + internal static int MsiCreateRecord(uint cParams, int hAny) + { + // When remoting is enabled, we might need to create either a local or + // remote record, depending on the handle it is to have an affinity with. + // If no affinity handle is specified, create a remote record (the 99% case). + if (!RemotingEnabled || + (hAny != 0 && !RemotableNativeMethods.IsRemoteHandle(hAny))) + { + return NativeMethods.MsiCreateRecord(cParams); + } + else + { + int hRecord = unchecked((int)RemotableNativeMethods.MsiFunc_III( + RemoteMsiFunctionId.MsiCreateRecord, (int) cParams, 0, 0)); + return RemotableNativeMethods.MakeRemoteHandle(hRecord); + } + } + + internal static uint MsiRecordGetFieldCount(int hRecord) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord)) + return NativeMethods.MsiRecordGetFieldCount(hRecord); + else + { + return RemotableNativeMethods.MsiFunc_III( + RemoteMsiFunctionId.MsiRecordGetFieldCount, + RemotableNativeMethods.GetRemoteHandle(hRecord), + 0, + 0); + } + } + + internal static int MsiRecordGetInteger(int hRecord, uint iField) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord)) + return NativeMethods.MsiRecordGetInteger(hRecord, iField); + else + { + return unchecked ((int) RemotableNativeMethods.MsiFunc_III( + RemoteMsiFunctionId.MsiRecordGetInteger, + RemotableNativeMethods.GetRemoteHandle(hRecord), + (int) iField, + 0)); + } + } + + internal static uint MsiRecordGetString(int hRecord, uint iField, StringBuilder szValueBuf, ref uint cchValueBuf) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord)) + { + return NativeMethods.MsiRecordGetString(hRecord, iField, szValueBuf, ref cchValueBuf); + } + else + { + return RemotableNativeMethods.MsiFunc_II_S( + RemoteMsiFunctionId.MsiRecordGetString, + RemotableNativeMethods.GetRemoteHandle(hRecord), + (int) iField, + szValueBuf, + ref cchValueBuf); + } + } + + internal static uint MsiRecordSetInteger(int hRecord, uint iField, int iValue) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord)) + return NativeMethods.MsiRecordSetInteger(hRecord, iField, iValue); + else + { + return RemotableNativeMethods.MsiFunc_III( + RemoteMsiFunctionId.MsiRecordSetInteger, + RemotableNativeMethods.GetRemoteHandle(hRecord), + (int) iField, + iValue); + } + } + + internal static uint MsiRecordSetString(int hRecord, uint iField, string szValue) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord)) + return NativeMethods.MsiRecordSetString(hRecord, iField, szValue); + else + { + return RemotableNativeMethods.MsiFunc_IIS( + RemoteMsiFunctionId.MsiRecordSetString, + RemotableNativeMethods.GetRemoteHandle(hRecord), + (int) iField, + szValue); + } + } + + internal static int MsiGetActiveDatabase(int hInstall) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiGetActiveDatabase(hInstall); + else + { + int hDatabase = (int)RemotableNativeMethods.MsiFunc_III( + RemoteMsiFunctionId.MsiGetActiveDatabase, + RemotableNativeMethods.GetRemoteHandle(hInstall), + 0, + 0); + return RemotableNativeMethods.MakeRemoteHandle(hDatabase); + } + } + + internal static uint MsiDatabaseOpenView(int hDatabase, string szQuery, out int hView) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hDatabase)) + return NativeMethods.MsiDatabaseOpenView(hDatabase, szQuery, out hView); + else + { + uint err = RemotableNativeMethods.MsiFunc_ISII_I( + RemoteMsiFunctionId.MsiDatabaseOpenView, + RemotableNativeMethods.GetRemoteHandle(hDatabase), + szQuery, + 0, + 0, + out hView); + hView = RemotableNativeMethods.MakeRemoteHandle(hView); + return err; + } + } + + internal static uint MsiViewExecute(int hView, int hRecord) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hView)) + return NativeMethods.MsiViewExecute(hView, hRecord); + else + { + return RemotableNativeMethods.MsiFunc_III( + RemoteMsiFunctionId.MsiViewExecute, + RemotableNativeMethods.GetRemoteHandle(hView), + RemotableNativeMethods.GetRemoteHandle(hRecord), + 0); + } + } + + internal static uint MsiViewFetch(int hView, out int hRecord) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hView)) + return NativeMethods.MsiViewFetch(hView, out hRecord); + else + { + uint err = RemotableNativeMethods.MsiFunc_II_I( + RemoteMsiFunctionId.MsiViewFetch, + RemotableNativeMethods.GetRemoteHandle(hView), + 0, + out hRecord); + hRecord = RemotableNativeMethods.MakeRemoteHandle(hRecord); + return err; + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal static uint MsiViewModify(int hView, int iModifyMode, int hRecord) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hView)) + return NativeMethods.MsiViewModify(hView, iModifyMode, hRecord); + else + { + return RemotableNativeMethods.MsiFunc_III( + RemoteMsiFunctionId.MsiViewModify, + RemotableNativeMethods.GetRemoteHandle(hView), + iModifyMode, + RemotableNativeMethods.GetRemoteHandle(hRecord)); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal static int MsiViewGetError(int hView, StringBuilder szColumnNameBuffer, ref uint cchBuf) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hView)) + return NativeMethods.MsiViewGetError(hView, szColumnNameBuffer, ref cchBuf); + else + { + return unchecked ((int) RemotableNativeMethods.MsiFunc_II_S( + RemoteMsiFunctionId.MsiViewGetError, + RemotableNativeMethods.GetRemoteHandle(hView), + 0, + szColumnNameBuffer, + ref cchBuf)); + } + } + + internal static uint MsiViewGetColumnInfo(int hView, uint eColumnInfo, out int hRecord) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hView)) + return NativeMethods.MsiViewGetColumnInfo(hView, eColumnInfo, out hRecord); + else + { + uint err = RemotableNativeMethods.MsiFunc_II_I( + RemoteMsiFunctionId.MsiViewGetColumnInfo, + RemotableNativeMethods.GetRemoteHandle(hView), + (int) eColumnInfo, + out hRecord); + hRecord = RemotableNativeMethods.MakeRemoteHandle(hRecord); + return err; + } + } + + internal static uint MsiFormatRecord(int hInstall, int hRecord, StringBuilder szResultBuf, ref uint cchResultBuf) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord)) + return NativeMethods.MsiFormatRecord(hInstall, hRecord, szResultBuf, ref cchResultBuf); + else + { + return RemotableNativeMethods.MsiFunc_II_S( + RemoteMsiFunctionId.MsiFormatRecord, + RemotableNativeMethods.GetRemoteHandle(hInstall), + RemotableNativeMethods.GetRemoteHandle(hRecord), + szResultBuf, + ref cchResultBuf); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal static uint MsiRecordClearData(int hRecord) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord)) + return NativeMethods.MsiRecordClearData(hRecord); + else + { + return RemotableNativeMethods.MsiFunc_III( + RemoteMsiFunctionId.MsiRecordClearData, + RemotableNativeMethods.GetRemoteHandle(hRecord), + 0, + 0); + } + } + + internal static bool MsiRecordIsNull(int hRecord, uint iField) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord)) + return NativeMethods.MsiRecordIsNull(hRecord, iField); + else + { + return 0 != RemotableNativeMethods.MsiFunc_III( + RemoteMsiFunctionId.MsiRecordIsNull, + RemotableNativeMethods.GetRemoteHandle(hRecord), + (int) iField, + 0); + } + } + + internal static uint MsiDatabaseGetPrimaryKeys(int hDatabase, string szTableName, out int hRecord) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hDatabase)) + return NativeMethods.MsiDatabaseGetPrimaryKeys(hDatabase, szTableName, out hRecord); + else + { + uint err = RemotableNativeMethods.MsiFunc_ISII_I( + RemoteMsiFunctionId.MsiDatabaseGetPrimaryKeys, + RemotableNativeMethods.GetRemoteHandle(hDatabase), + szTableName, + 0, + 0, + out hRecord); + hRecord = RemotableNativeMethods.MakeRemoteHandle(hRecord); + return err; + } + } + + internal static uint MsiDatabaseIsTablePersistent(int hDatabase, string szTableName) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hDatabase)) + return NativeMethods.MsiDatabaseIsTablePersistent(hDatabase, szTableName); + else + { + return RemotableNativeMethods.MsiFunc_ISI( + RemoteMsiFunctionId.MsiDatabaseIsTablePersistent, + RemotableNativeMethods.GetRemoteHandle(hDatabase), + szTableName, + 0); + } + } + + internal static uint MsiDoAction(int hInstall, string szAction) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiDoAction(hInstall, szAction); + else + { + return RemotableNativeMethods.MsiFunc_ISI( + RemoteMsiFunctionId.MsiDoAction, + RemotableNativeMethods.GetRemoteHandle(hInstall), + szAction, + 0); + } + } + + internal static uint MsiEnumComponentCosts(int hInstall, string szComponent, uint dwIndex, int iState, StringBuilder lpDriveBuf, ref uint cchDriveBuf, out int iCost, out int iTempCost) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiEnumComponentCosts(hInstall, szComponent, dwIndex, iState, lpDriveBuf, ref cchDriveBuf, out iCost, out iTempCost); + else + { + return RemotableNativeMethods.MsiFunc_ISII_SII( + RemoteMsiFunctionId.MsiEvaluateCondition, + RemotableNativeMethods.GetRemoteHandle(hInstall), + szComponent, (int) dwIndex, iState, lpDriveBuf, ref cchDriveBuf, out iCost, out iTempCost); + } + } + + internal static uint MsiEvaluateCondition(int hInstall, string szCondition) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiEvaluateCondition(hInstall, szCondition); + else + { + return RemotableNativeMethods.MsiFunc_ISI( + RemoteMsiFunctionId.MsiEvaluateCondition, + RemotableNativeMethods.GetRemoteHandle(hInstall), + szCondition, + 0); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal static uint MsiGetComponentState(int hInstall, string szComponent, out int iInstalled, out int iAction) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiGetComponentState(hInstall, szComponent, out iInstalled, out iAction); + else + { + return RemotableNativeMethods.MsiFunc_IS_II( + RemoteMsiFunctionId.MsiGetComponentState, + RemotableNativeMethods.GetRemoteHandle(hInstall), + szComponent, + out iInstalled, + out iAction); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal static uint MsiGetFeatureCost(int hInstall, string szFeature, int iCostTree, int iState, out int iCost) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiGetFeatureCost(hInstall, szFeature, iCostTree, iState, out iCost); + else + { + return RemotableNativeMethods.MsiFunc_ISII_I( + RemoteMsiFunctionId.MsiGetFeatureCost, + RemotableNativeMethods.GetRemoteHandle(hInstall), + szFeature, + iCostTree, + iState, + out iCost); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal static uint MsiGetFeatureState(int hInstall, string szFeature, out int iInstalled, out int iAction) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiGetFeatureState(hInstall, szFeature, out iInstalled, out iAction); + else + { + return RemotableNativeMethods.MsiFunc_IS_II( + RemoteMsiFunctionId.MsiGetFeatureState, + RemotableNativeMethods.GetRemoteHandle(hInstall), + szFeature, + out iInstalled, + out iAction); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal static uint MsiGetFeatureValidStates(int hInstall, string szFeature, out uint dwInstalledState) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiGetFeatureValidStates(hInstall, szFeature, out dwInstalledState); + else + { + int iTemp; + uint ret = RemotableNativeMethods.MsiFunc_ISII_I( + RemoteMsiFunctionId.MsiGetFeatureValidStates, + RemotableNativeMethods.GetRemoteHandle(hInstall), + szFeature, + 0, + 0, + out iTemp); + dwInstalledState = (uint) iTemp; + return ret; + } + } + + internal static int MsiGetLanguage(int hInstall) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiGetLanguage(hInstall); + else + { + return unchecked((int)RemotableNativeMethods.MsiFunc_III( + RemoteMsiFunctionId.MsiGetLanguage, + RemotableNativeMethods.GetRemoteHandle(hInstall), + 0, + 0)); + } + } + + internal static int MsiGetLastErrorRecord(int hAny) + { + // When remoting is enabled, we might need to create either a local or + // remote record, depending on the handle it is to have an affinity with. + // If no affinity handle is specified, create a remote record (the 99% case). + if (!RemotingEnabled || + (hAny != 0 && !RemotableNativeMethods.IsRemoteHandle(hAny))) + { + return NativeMethods.MsiGetLastErrorRecord(); + } + else + { + int hRecord = unchecked((int) RemotableNativeMethods.MsiFunc_III( + RemoteMsiFunctionId.MsiGetLastErrorRecord, 0, 0, 0)); + return RemotableNativeMethods.MakeRemoteHandle(hRecord); + } + } + + internal static bool MsiGetMode(int hInstall, uint iRunMode) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiGetMode(hInstall, iRunMode); + else + { + return 0 != RemotableNativeMethods.MsiFunc_III( + RemoteMsiFunctionId.MsiGetMode, + RemotableNativeMethods.GetRemoteHandle(hInstall), + (int) iRunMode, + 0); + } + } + + internal static uint MsiGetSourcePath(int hInstall, string szFolder, StringBuilder szPathBuf, ref uint cchPathBuf) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiGetSourcePath(hInstall, szFolder, szPathBuf, ref cchPathBuf); + else + { + return RemotableNativeMethods.MsiFunc_IS_S( + RemoteMsiFunctionId.MsiGetSourcePath, + RemotableNativeMethods.GetRemoteHandle(hInstall), + szFolder, + szPathBuf, + ref cchPathBuf); + } + } + + internal static uint MsiGetSummaryInformation(int hDatabase, string szDatabasePath, uint uiUpdateCount, out int hSummaryInfo) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hDatabase)) + return NativeMethods.MsiGetSummaryInformation(hDatabase, szDatabasePath, uiUpdateCount, out hSummaryInfo); + else + { + uint err = RemotableNativeMethods.MsiFunc_ISII_I( + RemoteMsiFunctionId.MsiGetSummaryInformation, + RemotableNativeMethods.GetRemoteHandle(hDatabase), + szDatabasePath, + (int)uiUpdateCount, + 0, + out hSummaryInfo); + hSummaryInfo = RemotableNativeMethods.MakeRemoteHandle(hSummaryInfo); + return err; + } + } + + internal static uint MsiGetTargetPath(int hInstall, string szFolder, StringBuilder szPathBuf, ref uint cchPathBuf) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiGetTargetPath(hInstall, szFolder, szPathBuf, ref cchPathBuf); + else + { + return RemotableNativeMethods.MsiFunc_IS_S( + RemoteMsiFunctionId.MsiGetTargetPath, + RemotableNativeMethods.GetRemoteHandle(hInstall), + szFolder, + szPathBuf, + ref cchPathBuf); + } + } + + internal static uint MsiRecordDataSize(int hRecord, uint iField) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord)) + return NativeMethods.MsiRecordDataSize(hRecord, iField); + else + { + return RemotableNativeMethods.MsiFunc_III( + RemoteMsiFunctionId.MsiRecordDataSize, + RemotableNativeMethods.GetRemoteHandle(hRecord), + (int) iField, 0); + } + } + + internal static uint MsiRecordReadStream(int hRecord, uint iField, byte[] szDataBuf, ref uint cbDataBuf) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord)) + { + return NativeMethods.MsiRecordReadStream(hRecord, iField, szDataBuf, ref cbDataBuf); + } + else lock (RemotableNativeMethods.remotingDelegate) + { + ClearData(requestBuf); + unchecked + { + WriteInt(requestBuf, 0, RemotableNativeMethods.GetRemoteHandle(hRecord)); + WriteInt(requestBuf, 1, (int) iField); + WriteInt(requestBuf, 2, (int) cbDataBuf); + IntPtr resp; + remotingDelegate(RemoteMsiFunctionId.MsiRecordReadStream, requestBuf, out resp); + uint ret = (uint) ReadInt(resp, 0); + if (ret == 0) + { + cbDataBuf = (uint) ReadInt(resp, 2); + if (cbDataBuf > 0) + { + RemotableNativeMethods.ReadStream(resp, 1, szDataBuf, (int) cbDataBuf); + } + } + return ret; + } + } + } + + internal static uint MsiRecordSetStream(int hRecord, uint iField, string szFilePath) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hRecord)) + return NativeMethods.MsiRecordSetStream(hRecord, iField, szFilePath); + else + { + return RemotableNativeMethods.MsiFunc_IIS( + RemoteMsiFunctionId.MsiRecordSetStream, + RemotableNativeMethods.GetRemoteHandle(hRecord), + (int) iField, + szFilePath); + } + } + + internal static uint MsiSequence(int hInstall, string szTable, int iSequenceMode) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiSequence(hInstall, szTable, iSequenceMode); + else + { + return RemotableNativeMethods.MsiFunc_ISI( + RemoteMsiFunctionId.MsiSequence, + RemotableNativeMethods.GetRemoteHandle(hInstall), + szTable, + iSequenceMode); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal static uint MsiSetComponentState(int hInstall, string szComponent, int iState) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiSetComponentState(hInstall, szComponent, iState); + else + { + return RemotableNativeMethods.MsiFunc_ISI( + RemoteMsiFunctionId.MsiSetComponentState, + RemotableNativeMethods.GetRemoteHandle(hInstall), + szComponent, + iState); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal static uint MsiSetFeatureAttributes(int hInstall, string szFeature, uint dwAttributes) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiSetFeatureAttributes(hInstall, szFeature, dwAttributes); + else + { + return RemotableNativeMethods.MsiFunc_ISI( + RemoteMsiFunctionId.MsiSetFeatureAttributes, + RemotableNativeMethods.GetRemoteHandle(hInstall), + szFeature, + (int) dwAttributes); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal static uint MsiSetFeatureState(int hInstall, string szFeature, int iState) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiSetFeatureState(hInstall, szFeature, iState); + else + { + return RemotableNativeMethods.MsiFunc_ISI( + RemoteMsiFunctionId.MsiSetFeatureState, + RemotableNativeMethods.GetRemoteHandle(hInstall), szFeature, iState); + } + } + + internal static uint MsiSetInstallLevel(int hInstall, int iInstallLevel) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiSetInstallLevel(hInstall, iInstallLevel); + else + { + return RemotableNativeMethods.MsiFunc_III( + RemoteMsiFunctionId.MsiSetInstallLevel, + RemotableNativeMethods.GetRemoteHandle(hInstall), + iInstallLevel, + 0); + } + } + + internal static uint MsiSetMode(int hInstall, uint iRunMode, bool fState) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiSetMode(hInstall, iRunMode, fState); + else + { + return RemotableNativeMethods.MsiFunc_III( + RemoteMsiFunctionId.MsiSetMode, + RemotableNativeMethods.GetRemoteHandle(hInstall), + (int) iRunMode, + fState ? 1 : 0); + } + } + + internal static uint MsiSetTargetPath(int hInstall, string szFolder, string szFolderPath) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiSetTargetPath(hInstall, szFolder, szFolderPath); + else + { + return RemotableNativeMethods.MsiFunc_ISS( + RemoteMsiFunctionId.MsiSetTargetPath, + RemotableNativeMethods.GetRemoteHandle(hInstall), + szFolder, + szFolderPath); + } + } + + internal static uint MsiSummaryInfoGetProperty(int hSummaryInfo, uint uiProperty, out uint uiDataType, out int iValue, ref long ftValue, StringBuilder szValueBuf, ref uint cchValueBuf) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hSummaryInfo)) + { + return NativeMethods.MsiSummaryInfoGetProperty(hSummaryInfo, uiProperty, out uiDataType, out iValue, ref ftValue, szValueBuf, ref cchValueBuf); + } + else lock (RemotableNativeMethods.remotingDelegate) + { + ClearData(requestBuf); + WriteInt(requestBuf, 0, RemotableNativeMethods.GetRemoteHandle(hSummaryInfo)); + WriteInt(requestBuf, 1, (int) uiProperty); + IntPtr resp; + remotingDelegate(RemoteMsiFunctionId.MsiSummaryInfoGetProperty, requestBuf, out resp); + unchecked + { + uint ret = (uint) ReadInt(resp, 0); + if (ret == 0) + { + uiDataType = (uint) ReadInt(resp, 1); + switch ((VarEnum) uiDataType) + { + case VarEnum.VT_I2: + case VarEnum.VT_I4: + iValue = ReadInt(resp, 2); + break; + + case VarEnum.VT_FILETIME: + uint ftHigh = (uint) ReadInt(resp, 2); + uint ftLow = (uint) ReadInt(resp, 3); + ftValue = ((long) ftHigh) << 32 | ((long) ftLow); + iValue = 0; + break; + + case VarEnum.VT_LPSTR: + ReadString(resp, 2, szValueBuf, ref cchValueBuf); + iValue = 0; + break; + + default: + iValue = 0; + break; + } + } + else + { + uiDataType = 0; + iValue = 0; + } + return ret; + } + } + } + + internal static uint MsiVerifyDiskSpace(int hInstall) + { + if (!RemotingEnabled || !RemotableNativeMethods.IsRemoteHandle(hInstall)) + return NativeMethods.MsiVerifyDiskSpace(hInstall); + else + { + return RemotableNativeMethods.MsiFunc_III( + RemoteMsiFunctionId.MsiVerifyDiskSpace, + RemotableNativeMethods.GetRemoteHandle(hInstall), + 0, + 0); + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Session.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Session.cs new file mode 100644 index 000000000..03b89193e --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Session.cs @@ -0,0 +1,952 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Text; + + /// + /// The Session object controls the installation process. It opens the + /// install database, which contains the installation tables and data. + /// + ///

+ /// This object is associated with a standard set of action functions, + /// each performing particular operations on data from one or more tables. Additional + /// custom actions may be added for particular product installations. The basic engine + /// function is a sequencer that fetches sequential records from a designated sequence + /// table, evaluates any specified condition expression, and executes the designated + /// action. Actions not recognized by the engine are deferred to the UI handler object + /// for processing, usually dialog box sequences. + ///

+ /// Note that only one Session object can be opened by a single process. + ///

+ internal sealed class Session : InstallerHandle, IFormatProvider + { + private Database database; + private CustomActionData customActionData; + private bool sessionAccessValidated = false; + + internal Session(IntPtr handle, bool ownsHandle) + : base(handle, ownsHandle) + { + } + + /// + /// Gets the Database for the install session. + /// + /// the Session handle is invalid + /// the Database cannot be accessed + ///

+ /// Normally there is no need to close this Database object. The same object can be + /// used throughout the lifetime of the Session, and it will be closed when the Session + /// is closed. + ///

+ /// Win32 MSI API: + /// MsiGetActiveDatabase + ///

+ [SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations")] + public Database Database + { + get + { + if (this.database == null || this.database.IsClosed) + { + lock (this.Sync) + { + if (this.database == null || this.database.IsClosed) + { + this.ValidateSessionAccess(); + + int hDb = RemotableNativeMethods.MsiGetActiveDatabase((int) this.Handle); + if (hDb == 0) + { + throw new InstallerException(); + } + this.database = new Database((IntPtr) hDb, true, "", DatabaseOpenMode.ReadOnly); + } + } + } + return this.database; + } + } + + /// + /// Gets the numeric language ID used by the current install session. + /// + ///

+ /// Win32 MSI API: + /// MsiGetLanguage + ///

+ public int Language + { + get + { + return (int) RemotableNativeMethods.MsiGetLanguage((int) this.Handle); + } + } + + /// + /// Gets or sets the string value of a named installer property, as maintained by the + /// Session object in the in-memory Property table, or, if it is prefixed with a percent + /// sign (%), the value of a system environment variable for the current process. + /// + /// the Session handle is invalid + ///

+ /// Win32 MSI APIs: + /// MsiGetProperty, + /// MsiSetProperty + ///

+ public string this[string property] + { + get + { + if (string.IsNullOrWhiteSpace(property)) + { + throw new ArgumentNullException("property"); + } + + if (!this.sessionAccessValidated && + !Session.NonImmediatePropertyNames.Contains(property)) + { + this.ValidateSessionAccess(); + } + + StringBuilder buf = new StringBuilder(); + uint bufSize = 0; + uint ret = RemotableNativeMethods.MsiGetProperty((int) this.Handle, property, buf, ref bufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + buf.Capacity = (int) ++bufSize; + ret = RemotableNativeMethods.MsiGetProperty((int) this.Handle, property, buf, ref bufSize); + } + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return buf.ToString(); + } + + set + { + if (string.IsNullOrWhiteSpace(property)) + { + throw new ArgumentNullException("property"); + } + + this.ValidateSessionAccess(); + + if (value == null) + { + value = String.Empty; + } + + uint ret = RemotableNativeMethods.MsiSetProperty((int) this.Handle, property, value); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + } + + /// + /// Creates a new Session object from an integer session handle. + /// + /// Integer session handle + /// true to close the handle when this object is disposed or finalized + ///

+ /// This method is only provided for interop purposes. A Session object + /// should normally be obtained by calling + /// or . + ///

+ public static Session FromHandle(IntPtr handle, bool ownsHandle) + { + return new Session(handle, ownsHandle); + } + + /// + /// Performs any enabled logging operations and defers execution to the UI handler + /// object associated with the engine. + /// + /// Type of message to be processed + /// Contains message-specific fields + /// A message-dependent return value + /// the Session or Record handle is invalid + /// an invalid message kind is specified + /// the user exited the installation + /// the message-handler failed for an unknown reason + ///

+ /// Logging may be selectively enabled for the various message types. + /// See the method. + ///

+ /// If record field 0 contains a formatting string, it is used to format the data in + /// the other fields. Else if the message is an error, warning, or user message, an attempt + /// is made to find a message template in the Error table for the current database using the + /// error number found in field 1 of the record for message types and return values. + ///

+ /// The parameter may also include message-box flags from + /// the following enumerations: System.Windows.Forms.MessageBoxButtons, + /// System.Windows.Forms.MessageBoxDefaultButton, System.Windows.Forms.MessageBoxIcon. These + /// flags can be combined with the InstallMessage with a bitwise OR. + ///

+ /// Note, this method never returns Cancel or Error values. Instead, appropriate + /// exceptions are thrown in those cases. + ///

+ /// Win32 MSI API: + /// MsiProcessMessage + ///

+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] + public MessageResult Message(InstallMessage messageType, Record record) + { + if (record == null) + { + throw new ArgumentNullException("record"); + } + + int ret = RemotableNativeMethods.MsiProcessMessage((int) this.Handle, (uint) messageType, (int) record.Handle); + if (ret < 0) + { + throw new InstallerException(); + } + else if (ret == (int) MessageResult.Cancel) + { + throw new InstallCanceledException(); + } + return (MessageResult) ret; + } + + /// + /// Writes a message to the log, if logging is enabled. + /// + /// The line to be written to the log + ///

+ /// Win32 MSI API: + /// MsiProcessMessage + ///

+ public void Log(string msg) + { + if (msg == null) + { + throw new ArgumentNullException("msg"); + } + + using (Record rec = new Record(0)) + { + rec.FormatString = msg; + this.Message(InstallMessage.Info, rec); + } + } + + /// + /// Writes a formatted message to the log, if logging is enabled. + /// + /// The line to be written to the log, containing 0 or more format specifiers + /// An array containing 0 or more objects to be formatted + ///

+ /// Win32 MSI API: + /// MsiProcessMessage + ///

+ public void Log(string format, params object[] args) + { + this.Log(String.Format(CultureInfo.InvariantCulture, format, args)); + } + + /// + /// Evaluates a logical expression containing symbols and values. + /// + /// conditional expression + /// The result of the condition evaluation + /// the Session handle is invalid + /// the condition is null or empty + /// the conditional expression is invalid + ///

+ /// Win32 MSI API: + /// MsiEvaluateCondition + ///

+ public bool EvaluateCondition(string condition) + { + if (string.IsNullOrWhiteSpace(condition)) + { + throw new ArgumentNullException("condition"); + } + + uint value = RemotableNativeMethods.MsiEvaluateCondition((int) this.Handle, condition); + if (value == 0) + { + return false; + } + else if (value == 1) + { + return true; + } + else + { + throw new InvalidOperationException(); + } + } + + /// + /// Evaluates a logical expression containing symbols and values, specifying a default + /// value to be returned in case the condition is empty. + /// + /// conditional expression + /// value to return if the condition is empty + /// The result of the condition evaluation + /// the Session handle is invalid + /// the conditional expression is invalid + ///

+ /// Win32 MSI API: + /// MsiEvaluateCondition + ///

+ public bool EvaluateCondition(string condition, bool defaultValue) + { + if (condition == null) + { + throw new ArgumentNullException("condition"); + } + else if (condition.Length == 0) + { + return defaultValue; + } + else + { + this.ValidateSessionAccess(); + return this.EvaluateCondition(condition); + } + } + + /// + /// Formats a string containing installer properties. + /// + /// A format string containing property tokens + /// A formatted string containing property data + /// the Record handle is invalid + ///

+ /// Win32 MSI API: + /// MsiFormatRecord + ///

+ [SuppressMessage("Microsoft.Naming", "CA1719:ParameterNamesShouldNotMatchMemberNames")] + public string Format(string format) + { + if (format == null) + { + throw new ArgumentNullException("format"); + } + + using (Record formatRec = new Record(0)) + { + formatRec.FormatString = format; + return formatRec.ToString(this); + } + } + + /// + /// Returns a formatted string from record data. + /// + /// Record object containing a template and data to be formatted. + /// The template string must be set in field 0 followed by any referenced data parameters. + /// A formatted string containing the record data + /// the Record handle is invalid + ///

+ /// Win32 MSI API: + /// MsiFormatRecord + ///

+ public string FormatRecord(Record record) + { + if (record == null) + { + throw new ArgumentNullException("record"); + } + + return record.ToString(this); + } + + /// + /// Returns a formatted string from record data using a specified format. + /// + /// Record object containing a template and data to be formatted + /// Format string to be used instead of field 0 of the Record + /// A formatted string containing the record data + /// the Record handle is invalid + ///

+ /// Win32 MSI API: + /// MsiFormatRecord + ///

+ [Obsolete("This method is obsolete because it has undesirable side-effects. As an alternative, set the Record's " + + "FormatString property separately before calling the FormatRecord() override that takes only the Record parameter.")] + public string FormatRecord(Record record, string format) + { + if (record == null) + { + throw new ArgumentNullException("record"); + } + + return record.ToString(format, this); + } + + /// + /// Retrieves product properties (not session properties) from the product database. + /// + /// Value of the property, or an empty string if the property is not set. + ///

+ /// Note this is not the correct method for getting ordinary session properties. For that, + /// see the indexer on the Session class. + ///

+ /// Win32 MSI API: + /// MsiGetProductProperty + ///

+ public string GetProductProperty(string property) + { + if (string.IsNullOrWhiteSpace(property)) + { + throw new ArgumentNullException("property"); + } + + this.ValidateSessionAccess(); + + StringBuilder buf = new StringBuilder(); + uint bufSize = (uint) buf.Capacity; + uint ret = NativeMethods.MsiGetProductProperty((int) this.Handle, property, buf, ref bufSize); + + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + buf.Capacity = (int) ++bufSize; + ret = NativeMethods.MsiGetProductProperty((int) this.Handle, property, buf, ref bufSize); + } + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return buf.ToString(); + } + + /// + /// Gets an accessor for components in the current session. + /// + public ComponentInfoCollection Components + { + get + { + this.ValidateSessionAccess(); + return new ComponentInfoCollection(this); + } + } + + /// + /// Gets an accessor for features in the current session. + /// + public FeatureInfoCollection Features + { + get + { + this.ValidateSessionAccess(); + return new FeatureInfoCollection(this); + } + } + + /// + /// Checks to see if sufficient disk space is present for the current installation. + /// + /// True if there is sufficient disk space; false otherwise. + ///

+ /// Win32 MSI API: + /// MsiVerifyDiskSpace + ///

+ public bool VerifyDiskSpace() + { + this.ValidateSessionAccess(); + + uint ret = RemotableNativeMethods.MsiVerifyDiskSpace((int)this.Handle); + if (ret == (uint) NativeMethods.Error.DISK_FULL) + { + return false; + } + else if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return true; + } + + /// + /// Gets the total disk space per drive required for the installation. + /// + /// A list of InstallCost structures, specifying the cost for each drive + ///

+ /// Win32 MSI API: + /// MsiEnumComponentCosts + ///

+ [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] + public IList GetTotalCost() + { + this.ValidateSessionAccess(); + + IList costs = new List(); + StringBuilder driveBuf = new StringBuilder(20); + for (uint i = 0; true; i++) + { + int cost, tempCost; + uint driveBufSize = (uint) driveBuf.Capacity; + uint ret = RemotableNativeMethods.MsiEnumComponentCosts( + (int) this.Handle, + null, + i, + (int) InstallState.Default, + driveBuf, + ref driveBufSize, + out cost, + out tempCost); + if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) break; + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + driveBuf.Capacity = (int) ++driveBufSize; + ret = RemotableNativeMethods.MsiEnumComponentCosts( + (int) this.Handle, + null, + i, + (int) InstallState.Default, + driveBuf, + ref driveBufSize, + out cost, + out tempCost); + } + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + costs.Add(new InstallCost(driveBuf.ToString(), cost * 512L, tempCost * 512L)); + } + return costs; + } + + /// + /// Gets the designated mode flag for the current install session. + /// + /// The type of mode to be checked. + /// The value of the designated mode flag. + /// the Session handle is invalid + /// an invalid mode flag was specified + ///

+ /// Note that only the following run modes are available to read from + /// a deferred custom action: + /// + /// + /// + /// + ///

+ /// Win32 MSI API: + /// MsiGetMode + ///

+ public bool GetMode(InstallRunMode mode) + { + return RemotableNativeMethods.MsiGetMode((int) this.Handle, (uint) mode); + } + + /// + /// Sets the designated mode flag for the current install session. + /// + /// The type of mode to be set. + /// The desired value of the mode. + /// the Session handle is invalid + /// an invalid mode flag was specified + /// the mode cannot not be set + ///

+ /// Win32 MSI API: + /// MsiSetMode + ///

+ public void SetMode(InstallRunMode mode, bool value) + { + this.ValidateSessionAccess(); + + uint ret = RemotableNativeMethods.MsiSetMode((int) this.Handle, (uint) mode, value); + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.ACCESS_DENIED) + { + throw new InvalidOperationException(); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + } + + /// + /// Gets the full path to the designated folder on the source media or server image. + /// + /// the folder was not found in the Directory table + /// the Session handle is invalid + ///

+ /// Win32 MSI API: + /// MsiGetSourcePath + ///

+ public string GetSourcePath(string directory) + { + if (string.IsNullOrWhiteSpace(directory)) + { + throw new ArgumentNullException("directory"); + } + + this.ValidateSessionAccess(); + + StringBuilder buf = new StringBuilder(); + uint bufSize = 0; + uint ret = RemotableNativeMethods.MsiGetSourcePath((int) this.Handle, directory, buf, ref bufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + buf.Capacity = (int) ++bufSize; + ret = ret = RemotableNativeMethods.MsiGetSourcePath((int) this.Handle, directory, buf, ref bufSize); + } + + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.DIRECTORY) + { + throw InstallerException.ExceptionFromReturnCode(ret, directory); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + return buf.ToString(); + } + + /// + /// Gets the full path to the designated folder on the installation target drive. + /// + /// the folder was not found in the Directory table + /// the Session handle is invalid + ///

+ /// Win32 MSI API: + /// MsiGetTargetPath + ///

+ public string GetTargetPath(string directory) + { + if (string.IsNullOrWhiteSpace(directory)) + { + throw new ArgumentNullException("directory"); + } + + this.ValidateSessionAccess(); + + StringBuilder buf = new StringBuilder(); + uint bufSize = 0; + uint ret = RemotableNativeMethods.MsiGetTargetPath((int) this.Handle, directory, buf, ref bufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + buf.Capacity = (int) ++bufSize; + ret = ret = RemotableNativeMethods.MsiGetTargetPath((int) this.Handle, directory, buf, ref bufSize); + } + + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.DIRECTORY) + { + throw InstallerException.ExceptionFromReturnCode(ret, directory); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + return buf.ToString(); + } + + /// + /// Sets the full path to the designated folder on the installation target drive. + /// + /// the folder was not found in the Directory table + /// the Session handle is invalid + ///

+ /// Setting the target path of a directory changes the path specification for the directory + /// in the in-memory Directory table. Also, the path specifications of all other path objects + /// in the table that are either subordinate or equivalent to the changed path are updated + /// to reflect the change. The properties for each affected path are also updated. + ///

+ /// If an error occurs in this function, all updated paths and properties revert to + /// their previous values. Therefore, it is safe to treat errors returned by this function + /// as non-fatal. + ///

+ /// Do not attempt to configure the target path if the components using those paths + /// are already installed for the current user or for a different user. Check the + /// ProductState property before setting the target path to determine if the product + /// containing this component is installed. + ///

+ /// Win32 MSI API: + /// MsiSetTargetPath + ///

+ public void SetTargetPath(string directory, string value) + { + if (string.IsNullOrWhiteSpace(directory)) + { + throw new ArgumentNullException("directory"); + } + + if (value == null) + { + throw new ArgumentNullException("value"); + } + + this.ValidateSessionAccess(); + + uint ret = RemotableNativeMethods.MsiSetTargetPath((int) this.Handle, directory, value); + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.DIRECTORY) + { + throw InstallerException.ExceptionFromReturnCode(ret, directory); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + } + + /// + /// Sets the install level for the current installation to a specified value and + /// recalculates the Select and Installed states for all features in the Feature + /// table. Also sets the Action state of each component in the Component table based + /// on the new level. + /// + /// New install level + /// the Session handle is invalid + ///

+ /// The SetInstallLevel method sets the following: + /// The installation level for the current installation to a specified value + /// The Select and Installed states for all features in the Feature table + /// The Action state of each component in the Component table, based on the new level + /// + /// If 0 or a negative number is passed in the ilnstallLevel parameter, + /// the current installation level does not change, but all features are still + /// updated based on the current installation level. + ///

+ /// Win32 MSI API: + /// MsiSetInstallLevel + ///

+ public void SetInstallLevel(int installLevel) + { + this.ValidateSessionAccess(); + + uint ret = RemotableNativeMethods.MsiSetInstallLevel((int) this.Handle, installLevel); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Executes a built-in action, custom action, or user-interface wizard action. + /// + /// Name of the action to execute. Case-sensitive. + /// the Session handle is invalid + /// the user exited the installation + ///

+ /// The DoAction method executes the action that corresponds to the name supplied. If the + /// name is not recognized by the installer as a built-in action or as a custom action in + /// the CustomAction table, the name is passed to the user-interface handler object, which + /// can invoke a function or a dialog box. If a null action name is supplied, the installer + /// uses the upper-case value of the ACTION property as the action to perform. If no property + /// value is defined, the default action is performed, defined as "INSTALL". + ///

+ /// Actions that update the system, such as the InstallFiles and WriteRegistryValues + /// actions, cannot be run by calling MsiDoAction. The exception to this rule is if DoAction + /// is called from a custom action that is scheduled in the InstallExecuteSequence table + /// between the InstallInitialize and InstallFinalize actions. Actions that do not update the + /// system, such as AppSearch or CostInitialize, can be called. + ///

+ /// Win32 MSI API: + /// MsiDoAction + ///

+ public void DoAction(string action) + { + this.DoAction(action, null); + } + + /// + /// Executes a built-in action, custom action, or user-interface wizard action. + /// + /// Name of the action to execute. Case-sensitive. + /// Optional data to be passed to a deferred custom action. + /// the Session handle is invalid + /// the user exited the installation + ///

+ /// The DoAction method executes the action that corresponds to the name supplied. If the + /// name is not recognized by the installer as a built-in action or as a custom action in + /// the CustomAction table, the name is passed to the user-interface handler object, which + /// can invoke a function or a dialog box. If a null action name is supplied, the installer + /// uses the upper-case value of the ACTION property as the action to perform. If no property + /// value is defined, the default action is performed, defined as "INSTALL". + ///

+ /// Actions that update the system, such as the InstallFiles and WriteRegistryValues + /// actions, cannot be run by calling MsiDoAction. The exception to this rule is if DoAction + /// is called from a custom action that is scheduled in the InstallExecuteSequence table + /// between the InstallInitialize and InstallFinalize actions. Actions that do not update the + /// system, such as AppSearch or CostInitialize, can be called. + ///

+ /// If the called action is a deferred, rollback, or commit custom action, then the supplied + /// will be available via the + /// property of that custom action's session. + ///

+ /// Win32 MSI API: + /// MsiDoAction + ///

+ public void DoAction(string action, CustomActionData actionData) + { + if (string.IsNullOrWhiteSpace(action)) + { + throw new ArgumentNullException("action"); + } + + this.ValidateSessionAccess(); + + if (actionData != null) + { + this[action] = actionData.ToString(); + } + + uint ret = RemotableNativeMethods.MsiDoAction((int) this.Handle, action); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Executes an action sequence described in the specified table. + /// + /// Name of the table containing the action sequence. + /// the Session handle is invalid + /// the user exited the installation + ///

+ /// This method queries the specified table, ordering the actions by the numbers in the Sequence column. + /// For each row retrieved, an action is executed, provided that any supplied condition expression does + /// not evaluate to FALSE. + ///

+ /// An action sequence containing any actions that update the system, such as the InstallFiles and + /// WriteRegistryValues actions, cannot be run by calling DoActionSequence. The exception to this rule is if + /// DoActionSequence is called from a custom action that is scheduled in the InstallExecuteSequence table + /// between the InstallInitialize and InstallFinalize actions. Actions that do not update the system, such + /// as AppSearch or CostInitialize, can be called. + ///

+ /// Win32 MSI API: + /// MsiSequence + ///

+ public void DoActionSequence(string sequenceTable) + { + if (string.IsNullOrWhiteSpace(sequenceTable)) + { + throw new ArgumentNullException("sequenceTable"); + } + + this.ValidateSessionAccess(); + + uint ret = RemotableNativeMethods.MsiSequence((int) this.Handle, sequenceTable, 0); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Gets custom action data for the session that was supplied by the caller. + /// + /// + public CustomActionData CustomActionData + { + get + { + if (this.customActionData == null) + { + this.customActionData = new CustomActionData(this[CustomActionData.PropertyName]); + } + + return this.customActionData; + } + } + + /// + /// Implements formatting for data. + /// + /// Type of format object to get. + /// The the current instance, if is the same type + /// as the current instance; otherwise, null. + object IFormatProvider.GetFormat(Type formatType) + { + return formatType == typeof(Session) ? this : null; + } + + /// + /// Closes the session handle. Also closes the active database handle, if it is open. + /// After closing a handle, further method calls may throw an . + /// + /// If true, the method has been called directly + /// or indirectly by a user's code, so managed and unmanaged resources will + /// be disposed. If false, only unmanaged resources will be disposed. + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (this.database != null) + { + this.database.Dispose(); + this.database = null; + } + } + } + finally + { + base.Dispose(disposing); + } + } + + /// + /// Gets the (short) list of properties that are available from non-immediate custom actions. + /// + private static IList NonImmediatePropertyNames + { + get + { + return new string[] { + CustomActionData.PropertyName, + "ProductCode", + "UserSID" + }; + } + } + + /// + /// Throws an exception if the custom action is not able to access immedate session details. + /// + private void ValidateSessionAccess() + { + if (!this.sessionAccessValidated) + { + if (this.GetMode(InstallRunMode.Scheduled) || + this.GetMode(InstallRunMode.Rollback) || + this.GetMode(InstallRunMode.Commit)) + { + throw new InstallerException("Cannot access session details from a non-immediate custom action"); + } + + this.sessionAccessValidated = true; + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ShortcutTarget.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ShortcutTarget.cs new file mode 100644 index 000000000..30068e1e2 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ShortcutTarget.cs @@ -0,0 +1,111 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Microsoft.Deployment.WindowsInstaller.ShortcutTarget structure. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + /// + /// Holds information about the target of a shortcut file. + /// + public struct ShortcutTarget + { + private string productCode; + private string feature; + private string componentCode; + + internal ShortcutTarget(string productCode, string feature, string componentCode) + { + this.productCode = productCode; + this.feature = feature; + this.componentCode = componentCode; + } + + /// + /// Gets the target product code of the shortcut, or null if not available. + /// + public string ProductCode + { + get + { + return this.productCode; + } + } + + /// + /// Gets the name of the target feature of the shortcut, or null if not available. + /// + public string Feature + { + get + { + return this.feature; + } + } + + /// + /// Gets the target component code of the shortcut, or null if not available. + /// + public string ComponentCode + { + get + { + return this.componentCode; + } + } + + /// + /// Tests whether two shortcut targets have the same product code, feature, and/or component code. + /// + /// The first shortcut target to compare. + /// The second shortcut target to compare. + /// True if all parts of the targets are the same, else false. + public static bool operator ==(ShortcutTarget st1, ShortcutTarget st2) + { + return st1.Equals(st2); + } + + /// + /// Tests whether two shortcut targets have the same product code, feature, and/or component code. + /// + /// The first shortcut target to compare. + /// The second shortcut target to compare. + /// True if any parts of the targets are different, else false. + public static bool operator !=(ShortcutTarget st1, ShortcutTarget st2) + { + return !st1.Equals(st2); + } + + /// + /// Tests whether two shortcut targets have the same product code, feature, and/or component code. + /// + /// The shortcut target to compare to the current object. + /// True if is a shortcut target and all parts of the targets are the same, else false. + public override bool Equals(object obj) + { + if (obj == null || obj.GetType() != typeof(ShortcutTarget)) + { + return false; + } + ShortcutTarget st = (ShortcutTarget) obj; + return this.productCode == st.productCode + && this.feature == st.feature + && this.componentCode == st.componentCode; + } + + /// + /// Generates a hash code using all parts of the shortcut target. + /// + /// An integer suitable for hashing the shortcut target. + public override int GetHashCode() + { + return (this.productCode != null ? this.productCode.GetHashCode() : 0) + ^ (this.feature != null ? this.feature.GetHashCode() : 0) + ^ (this.componentCode != null ? this.componentCode.GetHashCode() : 0); + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/SourceList.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/SourceList.cs new file mode 100644 index 000000000..a86c0095f --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/SourceList.cs @@ -0,0 +1,543 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Text; + + /// + /// A list of sources for an installed product or patch. + /// + [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] + internal class SourceList : ICollection + { + private Installation installation; + private SourceMediaList mediaList; + + internal SourceList(Installation installation) + { + this.installation = installation; + } + + /// + /// Gets the list of disks registered for the media source of + /// the patch or product installation. + /// + public SourceMediaList MediaList + { + get + { + if (this.mediaList == null) + { + this.mediaList = new SourceMediaList(this.installation); + } + return this.mediaList; + } + } + + /// + /// Gets the number of network and URL sources in the list. + /// + public int Count + { + get + { + int count = 0; + IEnumerator e = this.GetEnumerator(); + while (e.MoveNext()) + { + count++; + } + return count; + } + } + + /// + /// Gets a boolean value indicating whether the list is read-only. + /// A SourceList is never read-only. + /// + /// read-only status of the list + public bool IsReadOnly + { + get { return false; } + } + + /// + /// Adds a network or URL source to the source list of the installed product. + /// + /// Path to the source to be added. This parameter is + /// expected to contain only the path without the filename. + ///

+ /// If this method is called with a new source, the installer adds the source + /// to the end of the source list. + ///

+ /// If this method is called with a source already existing in the source + /// list, it has no effect. + ///

+ /// Win32 MSI APIs: + /// MsiSourceListAddSource, + /// MsiSourceListAddSourceEx + ///

+ /// + public void Add(string item) + { + if (!this.Contains(item)) + { + this.Insert(item, 0); + } + } + + /// + /// Adds or reorders a network or URL source for the product or patch. + /// + /// Path to the source to be added. This parameter is + /// expected to contain only the path without the filename. + /// Specifies the priority order in which the source + /// will be inserted + ///

+ /// If this method is called with a new source and + /// is set to 0, the installer adds the source to the end of the source list. + ///

+ /// If this method is called with a source already existing in the source + /// list and is set to 0, the installer retains the + /// source's existing index. + ///

+ /// If the method is called with an existing source in the source list + /// and is set to a non-zero value, the source is + /// removed from its current location in the list and inserted at the position + /// specified by Index, before any source that already exists at that position. + ///

+ /// If the method is called with a new source and Index is set to a + /// non-zero value, the source is inserted at the position specified by + /// , before any source that already exists at + /// that position. The index value for all sources in the list after the + /// index specified by Index are updated to ensure unique index values and + /// the pre-existing order is guaranteed to remain unchanged. + ///

+ /// If is greater than the number of sources + /// in the list, the source is placed at the end of the list with an index + /// value one larger than any existing source. + ///

+ /// Win32 MSI API: + /// MsiSourceListAddSourceEx + ///

+ public void Insert(string item, int index) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + NativeMethods.SourceType type = item.Contains("://") ? NativeMethods.SourceType.Url : NativeMethods.SourceType.Network; + + uint ret = NativeMethods.MsiSourceListAddSourceEx( + this.installation.InstallationCode, + this.installation.UserSid, + this.installation.Context, + (uint) type | (uint) this.installation.InstallationType, + item, + (uint) index); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Clears sources of all types: network, url, and media. + /// + ///

+ /// Win32 MSI API: + /// MsiSourceListClearAll + ///

+ public void Clear() + { + this.ClearSourceType(NativeMethods.SourceType.Url); + this.ClearSourceType(NativeMethods.SourceType.Network); + this.MediaList.Clear(); + } + + /// + /// Removes all network sources from the list. URL sources are not affected. + /// + ///

+ /// Win32 MSI API: + /// MsiSourceListClearAllEx + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void ClearNetworkSources() + { + this.ClearSourceType(NativeMethods.SourceType.Network); + } + + /// + /// Removes all URL sources from the list. Network sources are not affected. + /// + ///

+ /// Win32 MSI API: + /// MsiSourceListClearAllEx + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void ClearUrlSources() + { + this.ClearSourceType(NativeMethods.SourceType.Url); + } + + /// + /// Checks if the specified source exists in the list. + /// + /// case-insensitive source to look for + /// true if the source exists in the list, false otherwise + public bool Contains(string item) + { + if (string.IsNullOrWhiteSpace(item)) + { + throw new ArgumentNullException("item"); + } + + foreach (string s in this) + { + if (s.Equals(item, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } + + /// + /// Copies the network and URL sources from this list into an array. + /// + /// destination array to be filed + /// offset into the destination array where copying begins + public void CopyTo(string[] array, int arrayIndex) + { + if (array == null) { + throw new ArgumentNullException("array"); + } + foreach (string source in this) + { + array[arrayIndex++] = source; + } + } + + /// + /// Removes a network or URL source. + /// + ///

+ /// Win32 MSI API: + /// MsiSourceListClearSource + ///

+ public bool Remove(string item) + { + if (string.IsNullOrWhiteSpace(item)) + { + throw new ArgumentNullException("item"); + } + + NativeMethods.SourceType type = item.Contains("://") ? NativeMethods.SourceType.Url : NativeMethods.SourceType.Network; + + uint ret = NativeMethods.MsiSourceListClearSource( + this.installation.InstallationCode, + this.installation.UserSid, + this.installation.Context, + (uint) type | (uint) this.installation.InstallationType, + item); + if (ret != 0) + { + // TODO: Figure out when to return false. + throw InstallerException.ExceptionFromReturnCode(ret); + } + return true; + } + + /// + /// Enumerates the network and URL sources in the source list of the patch or product installation. + /// + ///

+ /// Win32 MSI API: + /// MsiSourceListEnumSources + ///

+ public IEnumerator GetEnumerator() + { + StringBuilder sourceBuf = new StringBuilder(256); + uint sourceBufSize = (uint) sourceBuf.Capacity; + for (uint i = 0; true; i++) + { + uint ret = this.EnumSources(sourceBuf, i, NativeMethods.SourceType.Network); + if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) + { + break; + } + else if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + else + { + yield return sourceBuf.ToString(); + } + } + + for (uint i = 0; true; i++) + { + uint ret = this.EnumSources(sourceBuf, i, NativeMethods.SourceType.Url); + if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) + { + break; + } + else if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + else + { + yield return sourceBuf.ToString(); + } + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + /// + /// Forces the installer to search the source list for a valid + /// source the next time a source is required. For example, when the + /// installer performs an installation or reinstallation, or when it + /// requires the path for a component that is set to run from source. + /// + ///

+ /// Win32 MSI APIs: + /// MsiSourceListForceResolution, + /// MsiSourceListForceResolutionEx + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void ForceResolution() + { + uint ret = NativeMethods.MsiSourceListForceResolutionEx( + this.installation.InstallationCode, + this.installation.UserSid, + this.installation.Context, + (uint) this.installation.InstallationType); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Gets or sets the path relative to the root of the installation media. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string MediaPackagePath + { + get + { + return this["MediaPackagePath"]; + } + set + { + this["MediaPackagePath"] = value; + } + } + + /// + /// Gets or sets the prompt template that is used when prompting the user + /// for installation media. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string DiskPrompt + { + get + { + return this["DiskPrompt"]; + } + set + { + this["DiskPrompt"] = value; + } + } + + /// + /// Gets or sets the most recently used source location for the product. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string LastUsedSource + { + get + { + return this["LastUsedSource"]; + } + set + { + this["LastUsedSource"] = value; + } + } + + /// + /// Gets or sets the name of the Windows Installer package or patch package + /// on the source. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string PackageName + { + get + { + return this["PackageName"]; + } + set + { + this["PackageName"] = value; + } + } + + /// + /// Gets the type of the last-used source. + /// + ///

+ ///

    + ///
  • "n" = network location
  • + ///
  • "u" = URL location
  • + ///
  • "m" = media location
  • + ///
  • (empty string) = no last used source
  • + ///
+ ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string LastUsedType + { + get + { + return this["LastUsedType"]; + } + } + + /// + /// Gets or sets source list information properties of a product or patch installation. + /// + /// The source list information property name. + /// An unknown product, patch, or property was requested + ///

+ /// Win32 MSI API: + /// MsiSourceListGetInfo + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string this[string property] + { + get + { + StringBuilder buf = new StringBuilder(""); + uint bufSize = 0; + uint ret = NativeMethods.MsiSourceListGetInfo( + this.installation.InstallationCode, + this.installation.UserSid, + this.installation.Context, + (uint) this.installation.InstallationType, + property, + buf, + ref bufSize); + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + buf.Capacity = (int) ++bufSize; + ret = NativeMethods.MsiSourceListGetInfo( + this.installation.InstallationCode, + this.installation.UserSid, + this.installation.Context, + (uint) this.installation.InstallationType, + property, + buf, + ref bufSize); + } + + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.UNKNOWN_PRODUCT || + ret == (uint) NativeMethods.Error.UNKNOWN_PROPERTY) + { + throw new ArgumentOutOfRangeException("property"); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + } + return buf.ToString(); + } + set + { + uint ret = NativeMethods.MsiSourceListSetInfo( + this.installation.InstallationCode, + this.installation.UserSid, + this.installation.Context, + (uint) this.installation.InstallationType, + property, + value); + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.UNKNOWN_PRODUCT || + ret == (uint) NativeMethods.Error.UNKNOWN_PROPERTY) + { + throw new ArgumentOutOfRangeException("property"); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + } + } + + private void ClearSourceType(NativeMethods.SourceType type) + { + uint ret = NativeMethods.MsiSourceListClearAllEx( + this.installation.InstallationCode, + this.installation.UserSid, + this.installation.Context, + (uint) type | (uint) this.installation.InstallationType); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + private uint EnumSources(StringBuilder sourceBuf, uint i, NativeMethods.SourceType sourceType) + { + int enumType = (this.installation.InstallationType | (int) sourceType); + uint sourceBufSize = (uint) sourceBuf.Capacity; + uint ret = NativeMethods.MsiSourceListEnumSources( + this.installation.InstallationCode, + this.installation.UserSid, + this.installation.Context, + (uint) enumType, + i, + sourceBuf, + ref sourceBufSize); + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + sourceBuf.Capacity = (int) ++sourceBufSize; + ret = NativeMethods.MsiSourceListEnumSources( + this.installation.InstallationCode, + this.installation.UserSid, + this.installation.Context, + (uint) enumType, + i, + sourceBuf, + ref sourceBufSize); + } + return ret; + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/SourceMediaList.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/SourceMediaList.cs new file mode 100644 index 000000000..99237113a --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/SourceMediaList.cs @@ -0,0 +1,238 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Text; + + /// + /// A list of source media for an installed product or patch. + /// + [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] + internal class SourceMediaList : ICollection + { + private Installation installation; + + internal SourceMediaList(Installation installation) + { + this.installation = installation; + } + + /// + /// Gets the number of source media in the list. + /// + public int Count + { + get + { + int count = 0; + IEnumerator e = this.GetEnumerator(); + while (e.MoveNext()) + { + count++; + } + return count; + } + } + + /// + /// Gets a boolean value indicating whether the list is read-only. + /// A SourceMediaList is never read-only. + /// + /// read-only status of the list + public bool IsReadOnly + { + get + { + return false; + } + } + + /// + /// Adds or updates a disk of the media source for the product or patch. + /// + ///

+ /// Win32 MSI API: + /// MsiSourceListAddMediaDisk + ///

+ public void Add(MediaDisk item) + { + uint ret = NativeMethods.MsiSourceListAddMediaDisk( + this.installation.InstallationCode, + this.installation.UserSid, + this.installation.Context, + (uint) this.installation.InstallationType, + (uint) item.DiskId, + item.VolumeLabel, + item.DiskPrompt); + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Removes all source media from the list. + /// + ///

+ /// Win32 MSI API: + /// MsiSourceListClearAllEx + ///

+ public void Clear() + { + uint ret = NativeMethods.MsiSourceListClearAllEx( + this.installation.InstallationCode, + this.installation.UserSid, + this.installation.Context, + (uint) NativeMethods.SourceType.Media | (uint) this.installation.InstallationType); + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Checks if the specified media disk id exists in the list. + /// + /// disk id of the media to look for + /// true if the media exists in the list, false otherwise + public bool Contains(int diskId) + { + foreach (MediaDisk mediaDisk in this) + { + if (mediaDisk.DiskId == diskId) + { + return true; + } + } + return false; + } + + bool ICollection.Contains(MediaDisk mediaDisk) + { + return this.Contains(mediaDisk.DiskId); + } + + /// + /// Copies the source media info from this list into an array. + /// + /// destination array to be filed + /// offset into the destination array where copying begins + public void CopyTo(MediaDisk[] array, int arrayIndex) + { + if (array == null) { + throw new ArgumentNullException("array"); + } + foreach (MediaDisk mediaDisk in this) + { + array[arrayIndex++] = mediaDisk; + } + } + + /// + /// Removes a specified disk from the set of registered disks. + /// + /// ID of the disk to remove + ///

+ /// Win32 MSI API: + /// MsiSourceListClearMediaDisk + ///

+ public bool Remove(int diskId) + { + uint ret = NativeMethods.MsiSourceListClearMediaDisk( + this.installation.InstallationCode, + this.installation.UserSid, + this.installation.Context, + (uint) this.installation.InstallationType, + (uint) diskId); + + if (ret != 0) + { + // TODO: Figure out when to return false. + throw InstallerException.ExceptionFromReturnCode(ret); + } + return true; + } + + bool ICollection.Remove(MediaDisk mediaDisk) + { + return this.Remove(mediaDisk.DiskId); + } + + /// + /// Enumerates the source media in the source list of the patch or product installation. + /// + ///

+ /// Win32 MSI API: + /// MsiSourceListEnumMediaDisks + ///

+ public IEnumerator GetEnumerator() + { + uint diskId; + StringBuilder volumeBuf = new StringBuilder(40); + uint volumeBufSize = (uint) volumeBuf.Capacity; + StringBuilder promptBuf = new StringBuilder(80); + uint promptBufSize = (uint) promptBuf.Capacity; + for (uint i = 0; true; i++) + { + uint ret = NativeMethods.MsiSourceListEnumMediaDisks( + this.installation.InstallationCode, + this.installation.UserSid, + this.installation.Context, + (uint) this.installation.InstallationType, + i, + out diskId, + volumeBuf, + ref volumeBufSize, + promptBuf, + ref promptBufSize); + + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + volumeBuf.Capacity = (int) ++volumeBufSize; + promptBuf.Capacity = (int) ++promptBufSize; + + ret = NativeMethods.MsiSourceListEnumMediaDisks( + this.installation.InstallationCode, + this.installation.UserSid, + this.installation.Context, + (uint) this.installation.InstallationType, + i, + out diskId, + volumeBuf, + ref volumeBufSize, + promptBuf, + ref promptBufSize); + } + + if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) + { + break; + } + + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + yield return new MediaDisk((int) diskId, volumeBuf.ToString(), promptBuf.ToString()); + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/SummaryInfo.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/SummaryInfo.cs new file mode 100644 index 000000000..e1211044a --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/SummaryInfo.cs @@ -0,0 +1,640 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Globalization; + using System.IO; + using System.Runtime.InteropServices; + using System.Text; + + /// + /// Provides access to summary information of a Windows Installer database. + /// + internal class SummaryInfo : InstallerHandle + { + internal const int MAX_PROPERTIES = 20; + + /// + /// Gets a SummaryInfo object that can be used to examine, update, and add + /// properties to the summary information stream of a package or transform. + /// + /// Path to the package (database) or transform + /// True to reserve resources for writing summary information properties. + /// the package does not exist or could not be read + /// the package is an invalid format + ///

+ /// The SummaryInfo object should be d after use. + /// It is best that the handle be closed manually as soon as it is no longer + /// needed, as leaving lots of unused handles open can degrade performance. + ///

+ /// Win32 MSI API: + /// MsiGetSummaryInformation + ///

+ public SummaryInfo(string packagePath, bool enableWrite) + : base((IntPtr) SummaryInfo.OpenSummaryInfo(packagePath, enableWrite), true) + { + } + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal SummaryInfo(IntPtr handle, bool ownsHandle) + : base(handle, ownsHandle) + { + } + + /// Gets or sets the Title summary information property. + ///

+ /// The Title summary information property briefly describes the type of installer package. Phrases + /// such as "Installation Database" or "Transform" or "Patch" may be used for this property. + ///

+ /// Win32 MSI APIs: + /// MsiSummaryInfoGetProperty, + /// MsiSummaryInfoSetProperty + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string Title + { + get { return this[2]; } + set { this[2] = value; } + } + + /// Gets or sets the Subject summary information property. + ///

+ /// The Subject summary information property conveys to a file browser the product that can be installed using + /// the logic and data in this installer database. For example, the value of the summary property for + /// Microsoft Office 97 would be "Microsoft Office 97 Professional". This value is typically set from the + /// installer property ProductName. + ///

+ /// Win32 MSI APIs: + /// MsiSummaryInfoGetProperty, + /// MsiSummaryInfoSetProperty + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string Subject + { + get { return this[3]; } + set { this[3] = value; } + } + + /// Gets or sets the Author summary information property. + ///

+ /// The Author summary information property conveys to a file browser the manufacturer of the installation + /// database. This value is typically set from the installer property Manufacturer. + ///

+ /// Win32 MSI APIs: + /// MsiSummaryInfoGetProperty, + /// MsiSummaryInfoSetProperty + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string Author + { + get { return this[4]; } + set { this[4] = value; } + } + + /// Gets or sets the Keywords summary information property. + ///

+ /// The Keywords summary information property is used by file browsers to hold keywords that permit the + /// database file to be found in a keyword search. The set of keywords typically includes "Installer" as + /// well as product-specific keywords, and may be localized. + ///

+ /// Win32 MSI APIs: + /// MsiSummaryInfoGetProperty, + /// MsiSummaryInfoSetProperty + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string Keywords + { + get { return this[5]; } + set { this[5] = value; } + } + + /// Gets or sets the Comments summary information property. + ///

+ /// The Comments summary information property conveys the general purpose of the installer database. By convention, + /// the value for this summary property is set to the following: + ///

+ /// "This installer database contains the logic and data required to install <product name>." + ///

+ /// where <product name> is the name of the product being installed. In general the value for this summary + /// property only changes in the product name, nothing else. + ///

+ /// Win32 MSI APIs: + /// MsiSummaryInfoGetProperty, + /// MsiSummaryInfoSetProperty + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string Comments + { + get { return this[6]; } + set { this[6] = value; } + } + + /// Gets or sets the Template summary information property. + ///

+ /// The Template summary information propery indicates the platform and language versions supported by the database. + ///

+ /// The syntax of the Template Summary property information is: + /// [platform property][,platform property][,...];[language id][,language id][,...] + ///

+ /// For example, the following are all valid values for the Template Summary property: + /// + /// Intel;1033 + /// Intel64;1033 + /// ;1033 + /// ; + /// Intel ;1033,2046 + /// Intel64;1033,2046 + /// Intel;0 + /// + ///

+ /// If this is a 64-bit Windows Installer, enter Intel64 in the Template summary information property. Note that an + /// installation package cannot have both the Intel and Intel64 properties set. + ///

+ /// If the current platform does not match one of the platforms specified then the installer will not process the + /// package. Not specifying a platform implies that the package is platform-independent. + ///

+ /// Entering 0 in the language ID field of the Template summary information property, or leaving this field empty, + /// indicates that the package is language neutral. + ///

+ /// There are variations of this property depending on whether it is in a source installer database or a transform. + ///

+ /// Source Installer Database - Only one language can be specified in a source installer database. Merge Modules are + /// the only packages that may have multiple languages. For more information, see Multiple Language Merge Modules. + ///

+ /// Transform - In a transform file, only one language may be specified. The specified platform and language determine + /// whether a transform can be applied to a particular database. The platform property and the language property can + /// be left blank if no transform restriction relies on them to validate the transform. + ///

+ /// This summary property is REQUIRED. + ///

+ /// Win32 MSI APIs: + /// MsiSummaryInfoGetProperty, + /// MsiSummaryInfoSetProperty + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string Template + { + get { return this[7]; } + set { this[7] = value; } + } + + /// Gets or sets the LastSavedBy summary information property. + ///

+ /// The installer sets the Last Saved By summary information property to the value of the LogonUser property during + /// an administrative installation. The installer never uses this property and a user never needs to modify it. + /// Developers of a database editing tool may use this property to track the last person to modify the database. + /// This property should be left set to null in a final shipping database. + ///

+ /// In a transform, this summary property contains the platform and language ID(s) that a database should have + /// after it has been transformed. The property specifies to what the Template should be set in the new database. + ///

+ /// Win32 MSI APIs: + /// MsiSummaryInfoGetProperty, + /// MsiSummaryInfoSetProperty + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string LastSavedBy + { + get { return this[8]; } + set { this[8] = value; } + } + + /// Gets or sets the RevisionNumber summary information property. + ///

+ /// The Revision Number summary information property contains the package code for the installer package. The + /// package code is a unique identifier of the installer package. + ///

+ /// The Revision Number summary information property of a patch package specifies the GUID patch code for + /// the patch. This is followed by a list of patch code GUIDs for obsolete patches that are removed when this + /// patch is applied. The patch codes are concatenated with no delimiters separating GUIDs in the list. + ///

+ /// The Revision Number summary information property of a transform package lists the product code GUIDs + /// and version of the new and original products and the upgrade code GUID. The list is separated with + /// semicolons as follows. + ///

+ /// Original-Product-Code Original-Product-Version ; New-Product Code New-Product-Version; Upgrade-Code + ///

+ /// This summary property is REQUIRED. + ///

+ /// Win32 MSI APIs: + /// MsiSummaryInfoGetProperty, + /// MsiSummaryInfoSetProperty + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string RevisionNumber + { + get { return this[9]; } + set { this[9] = value; } + } + + /// Gets or sets the CreatingApp summary information property. + ///

+ /// The CreatingApp summary information property conveys which application created the installer database. + /// In general the value for this summary property is the name of the software used to author this database. + ///

+ /// Win32 MSI APIs: + /// MsiSummaryInfoGetProperty, + /// MsiSummaryInfoSetProperty + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string CreatingApp + { + get { return this[18]; } + set { this[18] = value; } + } + + /// Gets or sets the LastPrintTime summary information property. + ///

+ /// The LastPrintTime summary information property can be set to the date and time during an administrative + /// installation to record when the administrative image was created. For non-administrative installations + /// this property is the same as the CreateTime summary information property. + ///

+ /// Win32 MSI APIs: + /// MsiSummaryInfoGetProperty, + /// MsiSummaryInfoSetProperty + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public DateTime LastPrintTime + { + get { return (DateTime) this[11, typeof(DateTime)]; } + set { this[11, typeof(DateTime)] = value; } + } + + /// Gets or sets the CreateTime summary information property. + ///

+ /// The CreateTime summary information property conveys when the installer database was created. + ///

+ /// Win32 MSI APIs: + /// MsiSummaryInfoGetProperty, + /// MsiSummaryInfoSetProperty + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public DateTime CreateTime + { + get { return (DateTime) this[12, typeof(DateTime)]; } + set { this[12, typeof(DateTime)] = value; } + } + + /// Gets or sets the LastSaveTime summary information property. + ///

+ /// The LastSaveTime summary information property conveys when the last time the installer database was + /// modified. Each time a user changes an installation the value for this summary property is updated to + /// the current system time/date at the time the installer database was saved. Initially the value for + /// this summary property is set to null to indicate that no changes have yet been made. + ///

+ /// Win32 MSI APIs: + /// MsiSummaryInfoGetProperty, + /// MsiSummaryInfoSetProperty + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public DateTime LastSaveTime + { + get { return (DateTime) this[13, typeof(DateTime)]; } + set { this[13, typeof(DateTime)] = value; } + } + + /// Gets or sets the CodePage summary information property. + ///

+ /// The Codepage summary information property is the numeric value of the ANSI code page used for any + /// strings that are stored in the summary information. Note that this is not the same code page for + /// strings in the installation database. The Codepage summary information property is used to translate + /// the strings in the summary information into Unicode when calling the Unicode API functions. The + /// Codepage summary information property must be set before any string properties are set in the + /// summary information. + ///

+ /// Win32 MSI APIs: + /// MsiSummaryInfoGetProperty, + /// MsiSummaryInfoSetProperty + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public short CodePage + { + get { return (short) this[1, typeof(short)]; } + set { this[1, typeof(short)] = value; } + } + + /// Gets or sets the PageCount summary information property. + ///

+ /// For an installation package, the PageCount summary information property contains the minimum + /// installer version required. For Windows Installer version 1.0, this property must be set to the + /// integer 100. For 64-bit Windows Installer Packages, this property must be set to the integer 200. + ///

+ /// For a transform package, the PageCount summary information property contains minimum installer + /// version required to process the transform. Set to the greater of the two PageCount summary information + /// property values belonging to the databases used to generate the transform. + ///

+ /// The PageCount summary information property is set to null in patch packages. + ///

+ /// This summary property is REQUIRED. + ///

+ /// Win32 MSI APIs: + /// MsiSummaryInfoGetProperty, + /// MsiSummaryInfoSetProperty + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public int PageCount + { + get { return (int) this[14, typeof(int)]; } + set { this[14, typeof(int)] = value; } + } + + /// Gets or sets the WordCount summary information property. + ///

+ /// The WordCount summary information property indicates the type of source file image. If this property is + /// not present, it defaults to 0. Note that this property is stored in place of the standard Count property. + ///

+ /// This property is a bit field. New bits may be added in the future. At present the following bits are + /// available: + /// + /// Bit 0: 0 = long file names, 1 = short file names + /// Bit 1: 0 = source is uncompressed, 1 = source is compressed + /// Bit 2: 0 = source is original media, 1 = source is administrative installation + /// [MSI 4.0] Bit 3: 0 = elevated privileges can be required to install, 1 = elevated privileges are not required to install + /// + ///

+ /// These are combined to give the WordCount summary information property one of the following values + /// indicating a type of source file image: + /// + /// 0 - Original source using long file names. Matches tree in Directory table. + /// 1 - Original source using short file names. Matches tree in Directory table. + /// 2 - Compressed source files using long file names. Matches cabinets and files in the Media table. + /// 3 - Compressed source files using short file names. Matches cabinets and files in the Media table. + /// 4 - Administrative image using long file names. Matches tree in Directory table. + /// 5 - Administrative image using short file names. Matches tree in Directory table. + /// + ///

+ /// Note that if the package is marked as compressed (bit 1 is set), the installer only installs files + /// located at the root of the source. In this case, even files marked as uncompressed in the File table must + /// be located at the root to be installed. To specify a source image that has both a cabinet file (compressed + /// files) and uncompressed files that match the tree in the Directory table, mark the package as uncompressed + /// by leaving bit 1 unset (value=0) in the WordCount summary information property and set + /// (value=16384) in the Attributes column of the File table + /// for each file in the cabinet. + ///

+ /// For a patch package, the WordCount summary information property specifies the patch engine that was used + /// to create the patch files. The default value is 1 and indicates that MSPATCH was used to create the patch + /// A value of "2" means that the patch is using smaller, optimized, files available only with Windows Installer + /// version 1.2 or later. A patch with a WordCount of "2" fails immediately if used with a Windows Installer + /// version earlier than 1.2. A patch with a WordCount of "3" fails immediately if used with a Windows Installer + /// version earlier than 2.0. + ///

+ /// This summary property is REQUIRED. + ///

+ /// Win32 MSI APIs: + /// MsiSummaryInfoGetProperty, + /// MsiSummaryInfoSetProperty + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public int WordCount + { + get { return (int) this[15, typeof(int)]; } + set { this[15, typeof(int)] = value; } + } + + /// Gets or sets the CharacterCount summary information property. + ///

+ /// The CharacterCount summary information property is only used in transforms. This part of the summary + /// information stream is divided into two 16-bit words. The upper word contains the transform validation + /// flags. The lower word contains the transform error condition flags. + ///

+ /// Win32 MSI APIs: + /// MsiSummaryInfoGetProperty, + /// MsiSummaryInfoSetProperty + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public int CharacterCount + { + get { return (int) this[16, typeof(int)]; } + set { this[16, typeof(int)] = value; } + } + + /// Gets or sets the Security summary information property. + ///

+ /// The Security summary information property conveys whether the package should be opened as read-only. The database + /// editing tool should not modify a read-only enforced database and should issue a warning at attempts to modify a + /// read-only recommended database. The following values of this property are applicable to Windows Installer files: + /// + /// 0 - no restriction + /// 2 - read only recommended + /// 4 - read only enforced + /// + ///

+ /// This property should be set to read-only recommended (2) for an installation database and to read-only + /// enforced (4) for a transform or patch. + ///

+ /// Win32 MSI APIs: + /// MsiSummaryInfoGetProperty, + /// MsiSummaryInfoSetProperty + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public int Security + { + get { return (int) this[19, typeof(int)]; } + set { this[19, typeof(int)] = value; } + } + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + private object this[uint property, Type type] + { + get + { + uint dataType; + StringBuilder stringValue = new StringBuilder(""); + uint bufSize = 0; + int intValue; + long timeValue = 0; + + uint ret = RemotableNativeMethods.MsiSummaryInfoGetProperty( + (int) this.Handle, + property, + out dataType, + out intValue, + ref timeValue, + stringValue, + ref bufSize); + if (ret != 0 && dataType != (uint) VarEnum.VT_LPSTR) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + switch ((VarEnum) dataType) + { + case VarEnum.VT_EMPTY: + { + if (type == typeof(DateTime)) + { + return DateTime.MinValue; + } + else if (type == typeof(string)) + { + return String.Empty; + } + else if (type == typeof(short)) + { + return (short) 0; + } + else + { + return (int) 0; + } + } + + case VarEnum.VT_LPSTR: + { + if (ret == (uint) NativeMethods.Error.MORE_DATA) + { + stringValue.Capacity = (int) ++bufSize; + ret = RemotableNativeMethods.MsiSummaryInfoGetProperty( + (int) this.Handle, + property, + out dataType, + out intValue, + ref timeValue, + stringValue, + ref bufSize); + } + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return stringValue.ToString(); + } + + case VarEnum.VT_I2: + case VarEnum.VT_I4: + { + if (type == typeof(string)) + { + return intValue.ToString(CultureInfo.InvariantCulture); + } + else if (type == typeof(short)) + { + return (short) intValue; + } + else + { + return intValue; + } + } + + case VarEnum.VT_FILETIME: + { + if (type == typeof(string)) + { + return DateTime.FromFileTime(timeValue).ToString(CultureInfo.InvariantCulture); + } + else + { + return DateTime.FromFileTime(timeValue); + } + } + + default: + { + throw new InstallerException(); + } + } + } + + set + { + uint dataType = (uint) VarEnum.VT_NULL; + string stringValue = ""; + int intValue = 0; + long timeValue = 0; + + if (type == typeof(short)) + { + dataType = (uint) VarEnum.VT_I2; + intValue = (int)(short) value; // Double cast because value is a *boxed* short. + } + else if (type == typeof(int)) + { + dataType = (uint) VarEnum.VT_I4; + intValue = (int) value; + } + else if (type == typeof(string)) + { + dataType = (uint) VarEnum.VT_LPSTR; + stringValue = (string) value; + } + else // (type == typeof(DateTime)) + { + dataType = (uint) VarEnum.VT_FILETIME; + timeValue = ((DateTime) value).ToFileTime(); + } + + uint ret = NativeMethods.MsiSummaryInfoSetProperty( + (int) this.Handle, + property, + dataType, + intValue, + ref timeValue, + stringValue); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + } + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + private string this[uint property] + { + get { return (string) this[property, typeof(string)]; } + set { this[property, typeof(string)] = value; } + } + + /// + /// Formats and writes the previously stored properties into the standard summary information stream. + /// + /// The stream cannot be successfully written. + ///

+ /// This method may only be called once after all the property values have been set. Properties may + /// still be read after the stream is written. + ///

+ /// Win32 MSI API: + /// MsiSummaryInfoPersist + ///

+ public void Persist() + { + uint ret = NativeMethods.MsiSummaryInfoPersist((int) this.Handle); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + private static int OpenSummaryInfo(string packagePath, bool enableWrite) + { + int summaryInfoHandle; + int maxProperties = !enableWrite ? 0 : SummaryInfo.MAX_PROPERTIES; + uint ret = RemotableNativeMethods.MsiGetSummaryInformation( + 0, + packagePath, + (uint) maxProperties, + out summaryInfoHandle); + if (ret != 0) + { + if (ret == (uint) NativeMethods.Error.FILE_NOT_FOUND || + ret == (uint) NativeMethods.Error.ACCESS_DENIED) + { + throw new FileNotFoundException(null, packagePath); + } + else + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + return summaryInfoHandle; + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/TableCollection.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/TableCollection.cs new file mode 100644 index 000000000..ea60244f7 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/TableCollection.cs @@ -0,0 +1,198 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections.Generic; + + /// + /// Contains information about all the tables in a Windows Installer database. + /// + internal class TableCollection : ICollection + { + private Database db; + + internal TableCollection(Database db) + { + this.db = db; + } + + /// + /// Gets the number of tables in the database. + /// + public int Count + { + get + { + return this.GetTables().Count; + } + } + + /// + /// Gets a boolean value indicating whether the collection is read-only. + /// A TableCollection is read-only when the database is read-only. + /// + /// read-only status of the collection + public bool IsReadOnly + { + get + { + return this.db.IsReadOnly; + } + } + + /// + /// Gets information about a given table. + /// + /// case-sensitive name of the table + /// information about the requested table, or null if the table does not exist in the database + public TableInfo this[string table] + { + get + { + if (string.IsNullOrWhiteSpace(table)) + { + throw new ArgumentNullException("table"); + } + + if (!this.Contains(table)) + { + return null; + } + + return new TableInfo(this.db, table); + } + } + + /// + /// Adds a new table to the database. + /// + /// information about the table to be added + /// a table with the same name already exists in the database + public void Add(TableInfo item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + if (this.Contains(item.Name)) + { + throw new InvalidOperationException(); + } + + this.db.Execute(item.SqlCreateString); + } + + /// + /// Removes all tables (and all data) from the database. + /// + public void Clear() + { + foreach (string table in this.GetTables()) + { + this.Remove(table); + } + } + + /// + /// Checks if the database contains a table with the given name. + /// + /// case-sensitive name of the table to search for + /// True if the table exists, false otherwise. + public bool Contains(string item) + { + if (string.IsNullOrWhiteSpace(item)) + { + throw new ArgumentNullException("item"); + } + uint ret = RemotableNativeMethods.MsiDatabaseIsTablePersistent((int) this.db.Handle, item); + if (ret == 3) // MSICONDITION_ERROR + { + throw new InstallerException(); + } + return ret != 2; // MSICONDITION_NONE + } + + bool ICollection.Contains(TableInfo item) + { + return this.Contains(item.Name); + } + + /// + /// Copies the table information from this collection into an array. + /// + /// destination array to be filed + /// offset into the destination array where copying begins + public void CopyTo(TableInfo[] array, int arrayIndex) + { + if (array == null) + { + throw new ArgumentNullException("array"); + } + + foreach (string table in this.GetTables()) + { + array[arrayIndex++] = new TableInfo(this.db, table); + } + } + + /// + /// Removes a table from the database. + /// + /// case-sensitive name of the table to be removed + /// true if the table was removed, false if the table did not exist + public bool Remove(string item) + { + if (string.IsNullOrWhiteSpace(item)) + { + throw new ArgumentNullException("item"); + } + + if (!this.Contains(item)) + { + return false; + } + this.db.Execute("DROP TABLE `{0}`", item); + return true; + } + + bool ICollection.Remove(TableInfo item) + { + if (item == null) + { + throw new ArgumentNullException("item"); + } + + return this.Remove(item.Name); + } + + /// + /// Enumerates the tables in the database. + /// + public IEnumerator GetEnumerator() + { + foreach (string table in this.GetTables()) + { + yield return new TableInfo(this.db, table); + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + private IList GetTables() + { + return this.db.ExecuteStringQuery("SELECT `Name` FROM `_Tables`"); + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/TableInfo.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/TableInfo.cs new file mode 100644 index 000000000..0cef3605e --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/TableInfo.cs @@ -0,0 +1,273 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Text; + + /// + /// Defines a table in an installation database. + /// + internal class TableInfo + { + private string name; + private ColumnCollection columns; + private ReadOnlyCollection primaryKeys; + + /// + /// Creates a table definition. + /// + /// Name of the table. + /// Columns in the table. + /// The primary keys of the table. + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public TableInfo(string name, ICollection columns, IList primaryKeys) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException("name"); + } + + if (columns == null || columns.Count == 0) + { + throw new ArgumentNullException("columns"); + } + + if (primaryKeys == null || primaryKeys.Count == 0) + { + throw new ArgumentNullException("primaryKeys"); + } + + this.name = name; + this.columns = new ColumnCollection(columns); + this.primaryKeys = new List(primaryKeys).AsReadOnly(); + foreach (string primaryKey in this.primaryKeys) + { + if (!this.columns.Contains(primaryKey)) + { + throw new ArgumentOutOfRangeException("primaryKeys"); + } + } + } + + internal TableInfo(Database db, string name) + { + if (db == null) + { + throw new ArgumentNullException("db"); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException("name"); + } + + this.name = name; + + using (View columnsView = db.OpenView("SELECT * FROM `{0}`", name)) + { + this.columns = new ColumnCollection(columnsView); + } + + this.primaryKeys = new ReadOnlyCollection( + TableInfo.GetTablePrimaryKeys(db, name)); + } + + /// + /// Gets the name of the table. + /// + public string Name + { + get + { + return this.name; + } + } + + /// + /// Gets information about the columns in this table. + /// + ///

+ /// This property queries the database every time it is called, + /// to ensure the returned values are up-to-date. For best performance, + /// hold onto the returned collection if using it more than once. + ///

+ public ColumnCollection Columns + { + get + { + return this.columns; + } + } + + /// + /// Gets the names of the columns that are primary keys of the table. + /// + public IList PrimaryKeys + { + get + { + return this.primaryKeys; + } + } + + /// + /// Gets an SQL CREATE string that can be used to create the table. + /// + public string SqlCreateString + { + get + { + StringBuilder s = new StringBuilder("CREATE TABLE `"); + s.Append(this.name); + s.Append("` ("); + int count = 0; + foreach (ColumnInfo col in this.Columns) + { + if (count > 0) + { + s.Append(", "); + } + s.Append(col.SqlCreateString); + count++; + } + s.Append(" PRIMARY KEY "); + count = 0; + foreach (string key in this.PrimaryKeys) + { + if (count > 0) + { + s.Append(", "); + } + s.AppendFormat("`{0}`", key); + count++; + } + s.Append(')'); + return s.ToString(); + } + } + + /// + /// Gets an SQL INSERT string that can be used insert a new record into the table. + /// + ///

+ /// The values are expressed as question-mark tokens, to be supplied by the record. + ///

+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string SqlInsertString + { + get + { + StringBuilder s = new StringBuilder("INSERT INTO `"); + s.Append(this.name); + s.Append("` ("); + int count = 0; + foreach (ColumnInfo col in this.Columns) + { + if (count > 0) + { + s.Append(", "); + } + + s.AppendFormat("`{0}`", col.Name); + count++; + } + s.Append(") VALUES ("); + while (count > 0) + { + count--; + s.Append("?"); + + if (count > 0) + { + s.Append(", "); + } + } + s.Append(')'); + return s.ToString(); + } + } + + /// + /// Gets an SQL SELECT string that can be used to select all columns of the table. + /// + ///

+ /// The columns are listed explicitly in the SELECT string, as opposed to using "SELECT *". + ///

+ public string SqlSelectString + { + get + { + StringBuilder s = new StringBuilder("SELECT "); + int count = 0; + foreach (ColumnInfo col in this.Columns) + { + if (count > 0) s.Append(", "); + s.AppendFormat("`{0}`", col.Name); + count++; + } + s.AppendFormat(" FROM `{0}`", this.Name); + return s.ToString(); + } + } + + /// + /// Gets a string representation of the table. + /// + /// The name of the table. + public override string ToString() + { + return this.name; + } + + private static IList GetTablePrimaryKeys(Database db, string table) + { + if (table == "_Tables") + { + return new string[] { "Name" }; + } + else if (table == "_Columns") + { + return new string[] { "Table", "Number" }; + } + else if (table == "_Storages") + { + return new string[] { "Name" }; + } + else if (table == "_Streams") + { + return new string[] { "Name" }; + } + else + { + int hrec; + uint ret = RemotableNativeMethods.MsiDatabaseGetPrimaryKeys( + (int) db.Handle, table, out hrec); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + using (Record rec = new Record((IntPtr) hrec, true, null)) + { + string[] keys = new string[rec.FieldCount]; + for (int i = 0; i < keys.Length; i++) + { + keys[i] = rec.GetString(i + 1); + } + + return keys; + } + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Transaction.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Transaction.cs new file mode 100644 index 000000000..90a095ae3 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/Transaction.cs @@ -0,0 +1,209 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Threading; + + /// + /// [MSI 4.5] Handle to a multi-session install transaction. + /// + ///

+ /// Win32 MSI APIs: + /// MsiBeginTransaction + /// MsiJoinTransaction + /// MsiEndTransaction + ///

+ internal class Transaction : InstallerHandle + { + private string name; + private IntPtr ownerChangeEvent; + private IList> ownerChangeListeners; + + /// + /// [MSI 4.5] Begins transaction processing of a multi-package installation. + /// + /// Name of the multi-package installation. + /// Select optional behavior when beginning the transaction. + /// The transaction could not be initialized. + ///

+ /// Win32 MSI API: + /// MsiBeginTransaction + ///

+ public Transaction(string name, TransactionAttributes attributes) + : this(name, Transaction.Begin(name, attributes), true) + { + } + + /// + /// Internal constructor. + /// + /// + /// The second parameter is an array in order to receive multiple values from the initialization method. + /// + private Transaction(string name, IntPtr[] handles, bool ownsHandle) + : base(handles[0], ownsHandle) + { + this.name = name; + this.ownerChangeEvent = handles[1]; + this.ownerChangeListeners = new List>(); + } + + /// + /// Creates a new Transaction object from an integer handle. + /// + /// Integer transaction handle + /// true to close the handle when this object is disposed + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public static Transaction FromHandle(IntPtr handle, bool ownsHandle) + { + return new Transaction(handle.ToString(), new IntPtr[] { handle, IntPtr.Zero }, ownsHandle); + } + + /// + /// Gets the name of the transaction. + /// + public string Name + { + get + { + return name; + } + } + + /// + /// Notifies listeners when the process that owns the transaction changed. + /// + public event EventHandler OwnerChanged + { + add + { + this.ownerChangeListeners.Add(value); + + if (this.ownerChangeEvent != IntPtr.Zero && this.ownerChangeListeners.Count == 1) + { + new Thread(this.WaitForOwnerChange).Start(); + } + } + remove + { + this.ownerChangeListeners.Remove(value); + } + } + + private void OnOwnerChanged() + { + EventArgs e = new EventArgs(); + foreach (EventHandler handler in this.ownerChangeListeners) + { + handler(this, e); + } + } + + private void WaitForOwnerChange() + { + int ret = NativeMethods.WaitForSingleObject(this.ownerChangeEvent, -1); + if (ret == 0) + { + this.OnOwnerChanged(); + } + else + { + throw new InstallerException(); + } + } + + /// + /// Makes the current process the owner of the multi-package installation transaction. + /// + /// Select optional behavior when joining the transaction. + /// The transaction handle is not valid. + /// The transaction could not be joined. + ///

+ /// Win32 MSI API: + /// MsiJoinTransaction + ///

+ public void Join(TransactionAttributes attributes) + { + IntPtr hChangeOfOwnerEvent; + uint ret = NativeMethods.MsiJoinTransaction((int) this.Handle, (int) attributes, out hChangeOfOwnerEvent); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + this.ownerChangeEvent = hChangeOfOwnerEvent; + if (this.ownerChangeEvent != IntPtr.Zero && this.ownerChangeListeners.Count >= 1) + { + new Thread(this.WaitForOwnerChange).Start(); + } + } + + /// + /// Ends the install transaction and commits all changes to the system belonging to the transaction. + /// + /// The transaction could not be committed. + ///

+ /// Runs any Commit Custom Actions and commits to the system any changes to Win32 or common language + /// runtime assemblies. Deletes the rollback script, and after using this option, the transaction's + /// changes can no longer be undone with a Rollback Installation. + ///

+ /// This method can only be called by the current owner of the transaction. + ///

+ /// Win32 MSI API: + /// MsiEndTransaction + ///

+ public void Commit() + { + this.End(true); + } + + /// + /// Ends the install transaction and undoes changes to the system belonging to the transaction. + /// + /// The transaction could not be rolled back. + ///

+ /// This method can only be called by the current owner of the transaction. + ///

+ /// Win32 MSI API: + /// MsiEndTransaction + ///

+ public void Rollback() + { + this.End(false); + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + private static IntPtr[] Begin(string transactionName, TransactionAttributes attributes) + { + int hTransaction; + IntPtr hChangeOfOwnerEvent; + uint ret = NativeMethods.MsiBeginTransaction(transactionName, (int) attributes, out hTransaction, out hChangeOfOwnerEvent); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + return new IntPtr[] { (IntPtr) hTransaction, hChangeOfOwnerEvent }; + } + + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] + private void End(bool commit) + { + uint ret = NativeMethods.MsiEndTransaction(commit ? 1 : 0); + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ValidationErrorInfo.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ValidationErrorInfo.cs new file mode 100644 index 000000000..b063cd4fa --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/ValidationErrorInfo.cs @@ -0,0 +1,54 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Microsoft.Deployment.WindowsInstaller.ValidationErrorInfo struct. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System.Diagnostics.CodeAnalysis; + + /// + /// Contains specific information about an error encountered by the , + /// , or methods of the + /// class. + /// + [SuppressMessage("Microsoft.Performance", "CA1815:OverrideEqualsAndOperatorEqualsOnValueTypes")] + public struct ValidationErrorInfo + { + private ValidationError error; + private string column; + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ValidationErrorInfo(ValidationError error, string column) + { + this.error = error; + this.column = column; + } + + /// + /// Gets the type of validation error encountered. + /// + public ValidationError Error + { + get + { + return this.error; + } + } + + /// + /// Gets the column containing the error, or null if the error applies to the whole row. + /// + public string Column + { + get + { + return this.column; + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/View.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/View.cs new file mode 100644 index 000000000..f336182aa --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/View.cs @@ -0,0 +1,648 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Text; + + /// + /// A View represents a result set obtained when processing a query using the + /// method of a + /// . Before any data can be transferred, + /// the query must be executed using the method, passing to + /// it all replaceable parameters designated within the SQL query string. + /// + [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] + internal class View : InstallerHandle, IEnumerable + { + private Database database; + private string sql; + private IList tables; + private ColumnCollection columns; + + internal View(IntPtr handle, string sql, Database database) + : base(handle, true) + { + this.sql = sql; + this.database = database; + } + + /// + /// Gets the Database on which this View was opened. + /// + public Database Database + { + get { return this.database; } + } + + /// + /// Gets the SQL query string used to open this View. + /// + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public string QueryString + { + get { return this.sql; } + } + + /// + /// Gets the set of tables that were included in the SQL query for this View. + /// + public IList Tables + { + get + { + if (this.tables == null) + { + if (this.sql == null) + { + return null; + } + + // Parse the table names out of the SQL query string by looking + // for tokens that can come before or after the list of tables. + + string parseSql = this.sql.Replace('\t', ' ').Replace('\r', ' ').Replace('\n', ' '); + string upperSql = parseSql.ToUpper(CultureInfo.InvariantCulture); + + string[] prefixes = new string[] { " FROM ", " INTO ", " TABLE " }; + string[] suffixes = new string[] { " WHERE ", " ORDER ", " SET ", " (", " ADD " }; + + int index; + + for (int i = 0; i < prefixes.Length; i++) + { + if ((index = upperSql.IndexOf(prefixes[i], StringComparison.Ordinal)) > 0) + { + parseSql = parseSql.Substring(index + prefixes[i].Length); + upperSql = upperSql.Substring(index + prefixes[i].Length); + } + } + + if (upperSql.StartsWith("UPDATE ", StringComparison.Ordinal)) + { + parseSql = parseSql.Substring(7); + upperSql = upperSql.Substring(7); + } + + for (int i = 0; i < suffixes.Length; i++) + { + if ((index = upperSql.IndexOf(suffixes[i], StringComparison.Ordinal)) > 0) + { + parseSql = parseSql.Substring(0, index); + upperSql = upperSql.Substring(0, index); + } + } + + if (upperSql.EndsWith(" HOLD", StringComparison.Ordinal) || + upperSql.EndsWith(" FREE", StringComparison.Ordinal)) + { + parseSql = parseSql.Substring(0, parseSql.Length - 5); + upperSql = upperSql.Substring(0, upperSql.Length - 5); + } + + // At this point we should be left with a comma-separated list of table names, + // optionally quoted with grave accent marks (`). + + string[] tableNames = parseSql.Split(','); + IList tableList = new List(tableNames.Length); + for (int i = 0; i < tableNames.Length; i++) + { + string tableName = tableNames[i].Trim(' ', '`'); + tableList.Add(new TableInfo(this.database, tableName)); + } + this.tables = tableList; + } + return new List(this.tables); + } + } + + /// + /// Gets the set of columns that were included in the query for this View, + /// or null if this view is not a SELECT query. + /// + /// the View is not in an active state + /// the View handle is invalid + ///

+ /// Win32 MSI API: + /// MsiViewGetColumnInfo + ///

+ public ColumnCollection Columns + { + get + { + if (this.columns == null) + { + this.columns = new ColumnCollection(this); + } + return this.columns; + } + } + + /// + /// Executes a SQL View query and supplies any required parameters. The query uses the + /// question mark token to represent parameters as described in SQL Syntax. The values of + /// these parameters are passed in as the corresponding fields of a parameter record. + /// + /// Optional Record that supplies the parameters. This + /// Record contains values to replace the parameter tokens in the SQL query. + /// the View could not be executed + /// the View handle is invalid + ///

+ /// Win32 MSI API: + /// MsiViewExecute + ///

+ [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Params"), SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] + public void Execute(Record executeParams) + { + uint ret = RemotableNativeMethods.MsiViewExecute( + (int) this.Handle, + (executeParams != null ? (int) executeParams.Handle : 0)); + if (ret == (uint) NativeMethods.Error.BAD_QUERY_SYNTAX) + { + throw new BadQuerySyntaxException(this.sql); + } + else if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Executes a SQL View query. + /// + /// the View could not be executed + /// the View handle is invalid + ///

+ /// Win32 MSI API: + /// MsiViewExecute + ///

+ public void Execute() { this.Execute(null); } + + /// + /// Fetches the next sequential record from the view, or null if there are no more records. + /// + /// the View is not in an active state + /// the View handle is invalid + ///

+ /// The Record object should be d after use. + /// It is best that the handle be closed manually as soon as it is no longer + /// needed, as leaving lots of unused handles open can degrade performance. + ///

+ /// Win32 MSI API: + /// MsiViewFetch + ///

+ public Record Fetch() + { + int recordHandle; + uint ret = RemotableNativeMethods.MsiViewFetch((int) this.Handle, out recordHandle); + if (ret == (uint) NativeMethods.Error.NO_MORE_ITEMS) + { + return null; + } + else if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + Record r = new Record((IntPtr) recordHandle, true, this); + r.IsFormatStringInvalid = true; + return r; + } + + /// + /// Updates a fetched Record. + /// + /// specifies the modify mode + /// the Record to modify + /// the modification failed, + /// or a validation was requested and the data did not pass + /// the View handle is invalid + ///

+ /// You can update or delete a record immediately after inserting, or seeking provided you + /// have NOT modified the 0th field of the inserted or sought record. + ///

+ /// To execute any SQL statement, a View must be created. However, a View that does not + /// create a result set, such as CREATE TABLE, or INSERT INTO, cannot be used with any of + /// the Modify methods to update tables though the view. + ///

+ /// You cannot fetch a record containing binary data from one database and then use + /// that record to insert the data into another database. To move binary data from one database + /// to another, you should export the data to a file and then import it into the new database + /// using a query and the . This ensures that each database has + /// its own copy of the binary data. + ///

+ /// Note that custom actions can only add, modify, or remove temporary rows, columns, + /// or tables from a database. Custom actions cannot modify persistent data in a database, + /// such as data that is a part of the database stored on disk. + ///

+ /// Win32 MSI API: + /// MsiViewModify + ///

+ /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void Modify(ViewModifyMode mode, Record record) + { + if (record == null) + { + throw new ArgumentNullException("record"); + } + + uint ret = RemotableNativeMethods.MsiViewModify((int) this.Handle, (int) mode, (int) record.Handle); + if (mode == ViewModifyMode.Insert || mode == ViewModifyMode.InsertTemporary) + { + record.IsFormatStringInvalid = true; + } + if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + } + + /// + /// Refreshes the data in a Record. + /// + /// the Record to be refreshed + /// the refresh failed + /// the View handle is invalid + ///

+ /// The Record must have been obtained by calling . Fails with + /// a deleted Record. Works only with read-write Records. + ///

+ /// See for more remarks. + ///

+ /// Win32 MSI API: + /// MsiViewModify + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void Refresh(Record record) { this.Modify(ViewModifyMode.Refresh, record); } + + /// + /// Inserts a Record into the view. + /// + /// the Record to be inserted + /// the insertion failed + /// the View handle is invalid + ///

+ /// Fails if a row with the same primary keys exists. Fails with a read-only database. + /// This method cannot be used with a View containing joins. + ///

+ /// See for more remarks. + ///

+ /// Win32 MSI API: + /// MsiViewModify + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void Insert(Record record) { this.Modify(ViewModifyMode.Insert, record); } + + /// + /// Updates the View with new data from the Record. + /// + /// the new data + /// the update failed + /// the View handle is invalid + ///

+ /// Only non-primary keys can be updated. The Record must have been obtained by calling + /// . Fails with a deleted Record. Works only with read-write Records. + ///

+ /// See for more remarks. + ///

+ /// Win32 MSI API: + /// MsiViewModify + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void Update(Record record) { this.Modify(ViewModifyMode.Update, record); } + + /// + /// Updates or inserts a Record into the View. + /// + /// the Record to be assigned + /// the assignment failed + /// the View handle is invalid + ///

+ /// Updates record if the primary keys match an existing row and inserts if they do not match. + /// Fails with a read-only database. This method cannot be used with a View containing joins. + ///

+ /// See for more remarks. + ///

+ /// Win32 MSI API: + /// MsiViewModify + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void Assign(Record record) { this.Modify(ViewModifyMode.Assign, record); } + + /// + /// Updates or deletes and inserts a Record into the View. + /// + /// the Record to be replaced + /// the replacement failed + /// the View handle is invalid + ///

+ /// The Record must have been obtained by calling . Updates record if the + /// primary keys are unchanged. Deletes old row and inserts new if primary keys have changed. + /// Fails with a read-only database. This method cannot be used with a View containing joins. + ///

+ /// See for more remarks. + ///

+ /// Win32 MSI API: + /// MsiViewModify + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void Replace(Record record) { this.Modify(ViewModifyMode.Replace, record); } + + /// + /// Deletes a Record from the View. + /// + /// the Record to be deleted + /// the deletion failed + /// the View handle is invalid + ///

+ /// The Record must have been obtained by calling . Fails if the row has been + /// deleted. Works only with read-write records. This method cannot be used with a View containing joins. + ///

+ /// See for more remarks. + ///

+ /// Win32 MSI API: + /// MsiViewModify + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void Delete(Record record) { this.Modify(ViewModifyMode.Delete, record); } + + /// + /// Inserts a Record into the View. The inserted data is not persistent. + /// + /// the Record to be inserted + /// the insertion failed + /// the View handle is invalid + ///

+ /// Fails if a row with the same primary key exists. Works only with read-write records. + /// This method cannot be used with a View containing joins. + ///

+ /// See for more remarks. + ///

+ /// Win32 MSI API: + /// MsiViewModify + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public void InsertTemporary(Record record) { this.Modify(ViewModifyMode.InsertTemporary, record); } + + /// + /// Refreshes the information in the supplied record without changing the position + /// in the result set and without affecting subsequent fetch operations. + /// + /// the Record to be filled with the result of the seek + /// the seek failed + /// the View handle is invalid + ///

+ /// After seeking, the Record may then be used for subsequent Update, Delete, and Refresh + /// operations. All primary key columns of the table must be in the query and the Record must + /// have at least as many fields as the query. Seek cannot be used with multi-table queries. + /// This method cannot be used with a View containing joins. + ///

+ /// See for more remarks. + ///

+ /// Win32 MSI API: + /// MsiViewModify + ///

+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool Seek(Record record) + { + if (record == null) + { + throw new ArgumentNullException("record"); + } + + uint ret = RemotableNativeMethods.MsiViewModify((int) this.Handle, (int) ViewModifyMode.Seek, (int) record.Handle); + record.IsFormatStringInvalid = true; + if (ret == (uint) NativeMethods.Error.FUNCTION_FAILED) + { + return false; + } + else if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + + return true; + } + + /// + /// Inserts or validates a record. + /// + /// the Record to be merged + /// true if the record was inserted or validated, false if there is an existing + /// record with the same primary keys that is not identical + /// the merge failed (for a reason other than invalid data) + /// the View handle is invalid + ///

+ /// Works only with read-write records. This method cannot be used with a + /// View containing joins. + ///

+ /// See for more remarks. + ///

+ /// Win32 MSI API: + /// MsiViewModify + ///

+ [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public bool Merge(Record record) + { + if (record == null) + { + throw new ArgumentNullException("record"); + } + + uint ret = RemotableNativeMethods.MsiViewModify((int) this.Handle, (int) ViewModifyMode.Merge, (int) record.Handle); + if (ret == (uint) NativeMethods.Error.FUNCTION_FAILED) + { + return false; + } + else if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return true; + } + + /// + /// Validates a record, returning information about any errors. + /// + /// the Record to be validated + /// null if the record was validated; if there is an existing record with + /// the same primary keys that has conflicting data then error information is returned + /// the validation failed (for a reason other than invalid data) + /// the View handle is invalid + ///

+ /// The Record must have been obtained by calling . + /// Works with read-write and read-only records. This method cannot be used + /// with a View containing joins. + ///

+ /// See for more remarks. + ///

+ /// Win32 MSI APIs: + /// MsiViewModify, + /// MsiViewGetError + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public ICollection Validate(Record record) { return this.InternalValidate(ViewModifyMode.Validate, record); } + + /// + /// Validates a new record, returning information about any errors. + /// + /// the Record to be validated + /// null if the record was validated; if there is an existing + /// record with the same primary keys then error information is returned + /// the validation failed (for a reason other than invalid data) + /// the View handle is invalid + ///

+ /// Checks for duplicate keys. The Record must have been obtained by + /// calling . Works with read-write and read-only records. + /// This method cannot be used with a View containing joins. + ///

+ /// See for more remarks. + ///

+ /// Win32 MSI APIs: + /// MsiViewModify, + /// MsiViewGetError + ///

+ [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public ICollection ValidateNew(Record record) { return this.InternalValidate(ViewModifyMode.ValidateNew, record); } + + /// + /// Validates fields of a fetched or new record, returning information about any errors. + /// Can validate one or more fields of an incomplete record. + /// + /// the Record to be validated + /// null if the record was validated; if there is an existing record with + /// the same primary keys that has conflicting data then error information is returned + /// the validation failed (for a reason other than invalid data) + /// the View handle is invalid + ///

+ /// Works with read-write and read-only records. This method cannot be used with + /// a View containing joins. + ///

+ /// See for more remarks. + ///

+ /// Win32 MSI APIs: + /// MsiViewModify, + /// MsiViewGetError + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public ICollection ValidateFields(Record record) { return this.InternalValidate(ViewModifyMode.ValidateField, record); } + + /// + /// Validates a record that will be deleted later, returning information about any errors. + /// + /// the Record to be validated + /// null if the record is safe to delete; if another row refers to + /// the primary keys of this row then error information is returned + /// the validation failed (for a reason other than invalid data) + /// the View handle is invalid + ///

+ /// Validation does not check for the existence of the primary keys of this row in properties + /// or strings. Does not check if a column is a foreign key to multiple tables. Works with + /// read-write and read-only records. This method cannot be used with a View containing joins. + ///

+ /// See for more remarks. + ///

+ /// Win32 MSI APIs: + /// MsiViewModify, + /// MsiViewGetError + ///

+ [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + public ICollection ValidateDelete(Record record) { return this.InternalValidate(ViewModifyMode.ValidateDelete, record); } + + /// + /// Enumerates over the Records retrieved by the View. + /// + /// An enumerator of Record objects. + /// The View was not d before attempting the enumeration. + ///

+ /// Each Record object should be d after use. + /// It is best that the handle be closed manually as soon as it is no longer + /// needed, as leaving lots of unused handles open can degrade performance. + /// However, note that it is not necessary to complete the enumeration just + /// for the purpose of closing handles, because Records are fetched lazily + /// on each step of the enumeration. + ///

+ /// Win32 MSI API: + /// MsiViewFetch + ///

+ public IEnumerator GetEnumerator() + { + Record rec; + while ((rec = this.Fetch()) != null) + { + yield return rec; + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + private ICollection InternalValidate(ViewModifyMode mode, Record record) + { + uint ret = RemotableNativeMethods.MsiViewModify((int) this.Handle, (int) mode, (int) record.Handle); + if (ret == (uint) NativeMethods.Error.INVALID_DATA) + { + ICollection errorInfo = new List(); + while (true) + { + uint bufSize = 40; + StringBuilder column = new StringBuilder("", (int) bufSize); + int error = RemotableNativeMethods.MsiViewGetError((int) this.Handle, column, ref bufSize); + if (error == -2 /*MSIDBERROR_MOREDATA*/) + { + column.Capacity = (int) ++bufSize; + error = RemotableNativeMethods.MsiViewGetError((int) this.Handle, column, ref bufSize); + } + + if (error == -3 /*MSIDBERROR_INVALIDARG*/) + { + throw InstallerException.ExceptionFromReturnCode((uint) NativeMethods.Error.INVALID_PARAMETER); + } + else if (error == 0 /*MSIDBERROR_NOERROR*/) + { + break; + } + + errorInfo.Add(new ValidationErrorInfo((ValidationError) error, column.ToString())); + } + + return errorInfo; + } + else if (ret != 0) + { + throw InstallerException.ExceptionFromReturnCode(ret); + } + return null; + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/customactiondata.cs b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/customactiondata.cs new file mode 100644 index 000000000..98c9f82f3 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Deployment/WindowsInstaller/customactiondata.cs @@ -0,0 +1,476 @@ +//--------------------------------------------------------------------- +// +// Copyright (c) 1999, Microsoft Corporation. All rights reserved. +// +// +// Part of the Deployment Tools Foundation project. +// +//--------------------------------------------------------------------- + +namespace Microsoft.PackageManagement.Msi.Internal.Deployment.WindowsInstaller +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Text; + using System.Xml; + using System.Xml.Serialization; + + /// + /// Contains a collection of key-value pairs suitable for passing between + /// immediate and deferred/rollback/commit custom actions. + /// + /// + /// Call the method to get a string + /// suitable for storing in a property and reconstructing the custom action data later. + /// + /// + /// + [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] + internal sealed class CustomActionData : IDictionary + { + /// + /// "CustomActionData" literal property name. + /// + public const string PropertyName = "CustomActionData"; + + private const char DataSeparator = ';'; + private const char KeyValueSeparator = '='; + + private IDictionary data; + + /// + /// Creates a new empty custom action data object. + /// + public CustomActionData() : this(null) + { + } + + /// + /// Reconstructs a custom action data object from data that was previously + /// persisted in a string. + /// + /// Previous output from . + public CustomActionData(string keyValueList) + { + this.data = new Dictionary(); + + if (keyValueList != null) + { + this.Parse(keyValueList); + } + } + + /// + /// Adds a key and value to the data collection. + /// + /// Case-sensitive data key. + /// Data value (may be null). + /// the key does not consist solely of letters, + /// numbers, and the period, underscore, and space characters. + public void Add(string key, string value) + { + CustomActionData.ValidateKey(key); + this.data.Add(key, value); + } + + /// + /// Adds a value to the data collection, using XML serialization to persist the object as a string. + /// + /// Case-sensitive data key. + /// Data value (may be null). + /// the key does not consist solely of letters, + /// numbers, and the period, underscore, and space characters. + /// The value type does not support XML serialization. + /// The value could not be serialized. + public void AddObject(string key, T value) + { + if (value == null) + { + this.Add(key, null); + } + else if (typeof(T) == typeof(string) || + typeof(T) == typeof(CustomActionData)) // Serialize nested CustomActionData + { + this.Add(key, value.ToString()); + } + else + { + string valueString = CustomActionData.Serialize(value); + this.Add(key, valueString); + } + } + + /// + /// Gets a value from the data collection, using XML serialization to load the object from a string. + /// + /// Case-sensitive data key. + /// The value could not be deserialized. + [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")] + public T GetObject(string key) + { + string value = this[key]; + if (value == null) + { + return default(T); + } + else if (typeof(T) == typeof(string)) + { + // Funny casting because the compiler doesn't know T is string here. + return (T) (object) value; + } + else if (typeof(T) == typeof(CustomActionData)) + { + // Deserialize nested CustomActionData. + return (T) (object) new CustomActionData(value); + } + else if (value.Length == 0) + { + return default(T); + } + else + { + return CustomActionData.Deserialize(value); + } + } + + /// + /// Determines whether the data contains an item with the specified key. + /// + /// Case-sensitive data key. + /// true if the data contains an item with the key; otherwise, false + public bool ContainsKey(string key) + { + return this.data.ContainsKey(key); + } + + /// + /// Gets a collection object containing all the keys of the data. + /// + public ICollection Keys + { + get + { + return this.data.Keys; + } + } + + /// + /// Removes the item with the specified key from the data. + /// + /// Case-sensitive data key. + /// true if the item was successfully removed from the data; + /// false if an item with the specified key was not found + public bool Remove(string key) + { + return this.data.Remove(key); + } + + /// + /// Gets the value with the specified key. + /// + /// Case-sensitive data key. + /// Value associated with the specified key, or + /// null if an item with the specified key was not found + /// true if the data contains an item with the specified key; otherwise, false. + public bool TryGetValue(string key, out string value) + { + return this.data.TryGetValue(key, out value); + } + + /// + /// Gets a collection containing all the values of the data. + /// + public ICollection Values + { + get + { + return this.data.Values; + } + } + + /// + /// Gets or sets a data value with a specified key. + /// + /// Case-sensitive data key. + /// the key does not consist solely of letters, + /// numbers, and the period, underscore, and space characters. + public string this[string key] + { + get + { + return this.data[key]; + } + set + { + CustomActionData.ValidateKey(key); + this.data[key] = value; + } + } + + /// + /// Adds an item with key and value to the data collection. + /// + /// Case-sensitive data key, with a data value that may be null. + /// the key does not consist solely of letters, + /// numbers, and the period, underscore, and space characters. + public void Add(KeyValuePair item) + { + CustomActionData.ValidateKey(item.Key); + this.data.Add(item); + } + + /// + /// Removes all items from the data. + /// + public void Clear() + { + if (this.data.Count > 0) + { + this.data.Clear(); + } + } + + /// + /// Determines whether the data contains a specified item. + /// + /// The data item to locate. + /// true if the data contains the item; otherwise, false + public bool Contains(KeyValuePair item) + { + return this.data.Contains(item); + } + + /// + /// Copies the data to an array, starting at a particular array index. + /// + /// Destination array. + /// Index in the array at which copying begins. + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + this.data.CopyTo(array, arrayIndex); + } + + /// + /// Gets the number of items in the data. + /// + public int Count + { + get + { + return this.data.Count; + } + } + + /// + /// Gets a value indicating whether the data is read-only. + /// + public bool IsReadOnly + { + get + { + return false; + } + } + + /// + /// Removes an item from the data. + /// + /// The item to remove. + /// true if the item was successfully removed from the data; + /// false if the item was not found + public bool Remove(KeyValuePair item) + { + return this.data.Remove(item); + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An enumerator that can be used to iterate through the collection. + public IEnumerator> GetEnumerator() + { + return this.data.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An enumerator that can be used to iterate through the collection. + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return ((System.Collections.IEnumerable) this.data).GetEnumerator(); + } + + /// + /// Gets a string representation of the data suitable for persisting in a property. + /// + /// Data string in the form "Key1=Value1;Key2=Value2" + public override string ToString() + { + StringBuilder buf = new StringBuilder(); + + foreach (KeyValuePair item in this.data) + { + if (buf.Length > 0) + { + buf.Append(CustomActionData.DataSeparator); + } + + buf.Append(item.Key); + + if (item.Value != null) + { + buf.Append(CustomActionData.KeyValueSeparator); + buf.Append(CustomActionData.Escape(item.Value)); + } + } + + return buf.ToString(); + } + + /// + /// Ensures that a key contains valid characters. + /// + /// key to be validated + /// the key does not consist solely of letters, + /// numbers, and the period, underscore, and space characters. + private static void ValidateKey(string key) + { + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentNullException("key"); + } + + for (int i = 0; i < key.Length; i++) + { + char c = key[i]; + if (!Char.IsLetterOrDigit(c) && c != '_' && c != '.' && + !(i > 0 && i < key.Length - 1 && c == ' ')) + { + throw new ArgumentOutOfRangeException("key"); + } + } + } + + /// + /// Serializes a value into an XML string. + /// + /// Type of the value. + /// Value to be serialized. + /// Serialized value data as a string. + private static string Serialize(T value) + { + XmlWriterSettings xws = new XmlWriterSettings(); + xws.OmitXmlDeclaration = true; + + StringWriter sw = new StringWriter(CultureInfo.InvariantCulture); + using (XmlWriter xw = XmlWriter.Create(sw, xws)) + { + XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); + ns.Add(string.Empty, String.Empty); // Prevent output of any namespaces + + XmlSerializer ser = new XmlSerializer(typeof(T)); + ser.Serialize(xw, value, ns); + + return sw.ToString(); + } + } + + /// + /// Deserializes a value from an XML string. + /// + /// Expected type of the value. + /// Serialized value data. + /// Deserialized value object. + private static T Deserialize(string value) + { + StringReader sr = new StringReader(value); + using (XmlReader xr = XmlReader.Create(sr)) + { + XmlSerializer ser = new XmlSerializer(typeof(T)); + return (T) ser.Deserialize(xr); + } + } + + /// + /// Escapes a value string by doubling any data-separator (semicolon) characters. + /// + /// + /// Escaped value string + private static string Escape(string value) + { + value = value.Replace(String.Empty + CustomActionData.DataSeparator, String.Empty + CustomActionData.DataSeparator + CustomActionData.DataSeparator); + return value; + } + + /// + /// Unescapes a value string by undoubling any doubled data-separator (semicolon) characters. + /// + /// + /// Unescaped value string + private static string Unescape(string value) + { + value = value.Replace(String.Empty + CustomActionData.DataSeparator + CustomActionData.DataSeparator, String.Empty + CustomActionData.DataSeparator); + return value; + } + + /// + /// Loads key-value pairs from a string into the data collection. + /// + /// key-value pair list of the form returned by + private void Parse(string keyValueList) + { + int itemStart = 0; + while (itemStart < keyValueList.Length) + { + // Find the next non-escaped data separator. + int semi = itemStart - 2; + do + { + semi = keyValueList.IndexOf(CustomActionData.DataSeparator, semi + 2); + } + while (semi >= 0 && semi < keyValueList.Length - 1 && keyValueList[semi + 1] == CustomActionData.DataSeparator); + + if (semi < 0) + { + semi = keyValueList.Length; + } + + // Find the next non-escaped key-value separator. + int equals = itemStart - 2; + do + { + equals = keyValueList.IndexOf(CustomActionData.KeyValueSeparator, equals + 2); + } + while (equals >= 0 && equals < keyValueList.Length - 1 && keyValueList[equals + 1] == CustomActionData.KeyValueSeparator); + + if (equals < 0 || equals > semi) + { + equals = semi; + } + + string key = keyValueList.Substring(itemStart, equals - itemStart); + string value = null; + + // If there's a key-value separator before the next data separator, then the item has a value. + if (equals < semi) + { + value = keyValueList.Substring(equals + 1, semi - (equals + 1)); + value = CustomActionData.Unescape(value); + } + + // Add non-duplicate items to the collection. + if (key.Length > 0 && !this.data.ContainsKey(key)) + { + this.data.Add(key, value); + } + + // Move past the data separator to the next item. + itemStart = semi + 1; + } + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/MsiProvider.cs b/src/Microsoft.PackageManagement.MsiProvider/MsiProvider.cs new file mode 100644 index 000000000..a871a6f51 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/MsiProvider.cs @@ -0,0 +1,449 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Msi.Internal { + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using Deployment.WindowsInstaller; + using Deployment.WindowsInstaller.Package; + using PackageManagement.Internal; + using PackageManagement.Internal.Implementation; + using PackageManagement.Internal.Utility.Collections; + using PackageManagement.Internal.Utility.Extensions; + using PackageManagement.Internal.Utility.Versions; + using System.Management.Automation; + + public class MsiProvider { + /// + /// The name of this Package Provider + /// + internal const string ProviderName = "msi"; + + private static readonly Dictionary _features = new Dictionary { + {Constants.Features.SupportedExtensions, new[] {"msi", "msp"}}, + {Constants.Features.MagicSignatures, new[] {Constants.Signatures.OleCompoundDocument}} + }; + + private int _progressId; + + /// + /// Returns the name of the Provider. + /// + /// The name of this proivder (uses the constant declared at the top of the class) + public string GetPackageProviderName() { + return ProviderName; + } + + /// + /// Performs one-time initialization of the PROVIDER. + /// + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void InitializeProvider(Request request) { + if( request == null ) { + throw new ArgumentNullException("request"); + } + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::InitializeProvider'", ProviderName); + } + + /// + /// Returns a collection of strings to the client advertizing features this provider supports. + /// + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void GetFeatures(Request request) { + if( request == null ) { + throw new ArgumentNullException("request"); + } + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::GetFeatures' ", ProviderName); + + foreach (var feature in _features) { + request.Yield(feature); + } + } + + /// + /// Returns dynamic option definitions to the HOST + /// + /// The category of dynamic options that the HOST is interested in + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void GetDynamicOptions(string category, Request request) { + if( request == null ) { + throw new ArgumentNullException("request"); + } + category = category ?? string.Empty; + + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::GetDynamicOptions' '{1}'", ProviderName, category); + + switch (category.ToLowerInvariant()) { + case "install": + // options required for install/uninstall/getinstalledpackages + request.YieldDynamicOption("AdditionalArguments", "StringArray", false); + break; + + case "provider": + // options used with this provider. Not currently used. + break; + + case "source": + // options for package sources + break; + + case "package": + // options used when searching for packages + break; + } + } + + /// + /// Finds packages given a locally-accessible filename + /// Package information must be returned using request.YieldPackage(...) function. + /// + /// the full path to the file to determine if it is a package + /// + /// if this is greater than zero (and the number should have been generated using StartFind(...), + /// the core is calling this multiple times to do a batch search request. The operation can be delayed until + /// CompleteFind(...) is called + /// + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void FindPackageByFile(string file, int id, Request request) { + if( request == null ) { + throw new ArgumentNullException("request"); + } + if( string.IsNullOrWhiteSpace(file) ) { + throw new ArgumentNullException("file"); + } + + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::FindPackageByFile' '{1}','{2}'", ProviderName, file, id); + + if (!file.FileExists()) { + request.Error(Microsoft.PackageManagement.Internal.ErrorCategory.ObjectNotFound, file, Constants.Messages.UnableToResolvePackage, file); + return; + } + try { + var package = new InstallPackage(file, DatabaseOpenMode.ReadOnly); + YieldPackage(package, file, request); + package.Close(); + } catch (Exception e) { + e.Dump(); + // any exception at this point really just means that + request.Error(Microsoft.PackageManagement.Internal.ErrorCategory.OpenError, file, Constants.Messages.UnableToResolvePackage, file); + } + } + + /// + /// Returns the packages that are installed + /// + /// the package name to match. Empty or null means match everything + /// the specific version asked for. If this parameter is specified (ie, not null or empty string) then the minimum and maximum values are ignored + /// the minimum version of packages to return . If the requiredVersion parameter is specified (ie, not null or empty string) this should be ignored + /// the maximum version of packages to return . If the requiredVersion parameter is specified (ie, not null or empty string) this should be ignored + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void GetInstalledPackages(string name, string requiredVersion, string minimumVersion, string maximumVersion, Request request) { + if( request == null ) { + throw new ArgumentNullException("request"); + } + + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::GetInstalledPackages' '{1}','{2}','{3}','{4}'", ProviderName, name, requiredVersion, minimumVersion, maximumVersion); + var products = ProductInstallation.AllProducts; + WildcardPattern pattern = new WildcardPattern(name, WildcardOptions.IgnoreCase); + var installed = string.IsNullOrWhiteSpace(name) + ? products.Where(each => each.IsInstalled) : products.Where(each => each.IsInstalled && pattern.IsMatch(each.ProductName)); + + if (!string.IsNullOrWhiteSpace(requiredVersion)) { + // filter to just the exact version + var rv = new Version(requiredVersion.FixVersion()); + installed = installed.Where(each => (FourPartVersion)each.ProductVersion == (FourPartVersion)rv); + } else { + if (!string.IsNullOrWhiteSpace(minimumVersion)) { + var min = new Version(minimumVersion.FixVersion()); + installed = installed.Where(each => (FourPartVersion)each.ProductVersion >= (FourPartVersion)min); + } + if (!string.IsNullOrWhiteSpace(maximumVersion)) { + var max = new Version(maximumVersion.FixVersion()); + installed = installed.Where(each => (FourPartVersion)each.ProductVersion <= (FourPartVersion)max); + } + } + // make sure we don't enumerate more once + installed = installed.ReEnumerable(); + + // dump out results. + if (installed.Any(p => !YieldPackage(p, name, request))) { + } + } + + /// + /// Installs a given package. + /// + /// A provider supplied identifier that specifies an exact package + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void InstallPackage(string fastPackageReference, Request request) { + if( request == null ) { + throw new ArgumentNullException("request"); + } + if( string.IsNullOrWhiteSpace(fastPackageReference) ) { + throw new ArgumentNullException("fastPackageReference"); + } + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::InstallPackage' '{1}'", ProviderName, fastPackageReference); + var file = fastPackageReference.CanonicalizePath(false); + if (!file.FileExists()) { + request.Error(Microsoft.PackageManagement.Internal.ErrorCategory.OpenError, fastPackageReference, Constants.Messages.UnableToResolvePackage, fastPackageReference); + return; + } + string errorLogFolder = Path.GetTempPath() + Guid.NewGuid(); + DirectoryInfo errorDir = Directory.CreateDirectory(errorLogFolder); + string errorLogPath = errorLogFolder + "\\msi.log"; + try { + var package = new InstallPackage(file, DatabaseOpenMode.ReadOnly); + + Installer.SetInternalUI(InstallUIOptions.UacOnly | InstallUIOptions.Silent); + + // todo 1501: support additional parameters! + + if (request.Sources != null && request.Sources.Any()) { + // The 'file' can be from a temp location downloaded by a chained provider. In that case, we can show + // the orignal package source specified in the request.Sources. + _progressId = request.StartProgress(0, Resources.Messages.InstallingMSIPackage, request.Sources.FirstOrDefault()); + + } else { + _progressId = request.StartProgress(0, Resources.Messages.InstallingMSIPackage, file); + } + + var handler = CreateProgressHandler(request, Resources.Messages.Installing); + + Installer.SetExternalUI(handler, InstallLogModes.Progress | InstallLogModes.Info); + Installer.EnableLog(InstallLogModes.Error, errorLogPath); + Installer.InstallProduct(file, "REBOOT=REALLYSUPPRESS"); + Installer.SetInternalUI(InstallUIOptions.Default); + + Installer.SetExternalUI(handler, InstallLogModes.None); + + if (request.Sources != null && request.Sources.Any()) { + + // The 'file' can be from a temp location downloaded by a chained provider. In that case, we can show + // the orignal package source specified in the request.Sources. + YieldPackage(package, request.Sources.FirstOrDefault(), request); + } else { + YieldPackage(package, file, request); + } + + package.Close(); + + if (Installer.RebootRequired) { + request.Warning(Resources.Messages.InstallRequireReboot); + } + + if (errorDir.Exists) + errorDir.Delete(true); + } catch (Exception e) { + e.Dump(); + request.Error(Microsoft.PackageManagement.Internal.ErrorCategory.InvalidOperation, file, Constants.Messages.PackageFailedInstallErrorLog, file, errorLogPath); + } + + request.CompleteProgress(_progressId, true); + } + + /// + /// Uninstalls a package + /// + /// + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void UninstallPackage(string fastPackageReference, Request request) { + if( request == null ) { + throw new ArgumentNullException("request"); + } + if( string.IsNullOrWhiteSpace(fastPackageReference) ) { + throw new ArgumentNullException("fastPackageReference"); + } + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::UninstallPackage' '{1}'", ProviderName, fastPackageReference); + + try { + Guid guid; + if (!Guid.TryParse(fastPackageReference, out guid)) { + request.Error(Microsoft.PackageManagement.Internal.ErrorCategory.InvalidArgument, fastPackageReference, Constants.Messages.UnableToResolvePackage, fastPackageReference); + return; + } + var product = ProductInstallation.GetProducts(fastPackageReference, null, UserContexts.All).FirstOrDefault(); + if (product == null) { + request.Error(Microsoft.PackageManagement.Internal.ErrorCategory.InvalidArgument, fastPackageReference, Constants.Messages.UnableToResolvePackage, fastPackageReference); + return; + } + var productVersion = product.ProductVersion.ToString(); + var productName = product.ProductName; + var summary = product["Summary"]; + + Installer.SetInternalUI(InstallUIOptions.UacOnly | InstallUIOptions.Silent); + _progressId = request.StartProgress(0, Resources.Messages.UninstallingMSIPackage, productName); + var handler = CreateProgressHandler(request, Resources.Messages.UnInstalling); + + Installer.SetExternalUI(handler, InstallLogModes.Progress | InstallLogModes.Info); + Installer.InstallProduct(product.LocalPackage, "REMOVE=ALL REBOOT=REALLYSUPPRESS"); + Installer.SetInternalUI(InstallUIOptions.Default); + + Installer.SetExternalUI(handler, InstallLogModes.None); + + // YieldPackage(product,fastPackageReference, request); + if (request.YieldSoftwareIdentity(fastPackageReference, productName, productVersion, "multipartnumeric", summary, "", fastPackageReference, "", "") != null) { + request.AddMetadata(fastPackageReference, "ProductCode", fastPackageReference); + request.AddTagId(fastPackageReference.Trim(new char[] { '{', '}' })); + } + + + request.Warning(Resources.Messages.UninstallRequireReboot); + } catch (Exception e) { + e.Dump(); + } + request.CompleteProgress(_progressId, true); + _progressId = 0; + } + + private ExternalUIHandler CreateProgressHandler(Request request, string showMessage) { + var currentTotalTicks = -1; + var currentProgress = 0; + var progressDirection = 1; + var actualPercent = 0; + + ExternalUIHandler handler = (type, message, buttons, icon, button) => { + if (request.IsCanceled) { + return MessageResult.Cancel; + } + + switch (type) { + case InstallMessage.Progress: + if (message.Length >= 2) { + var msg = message.Split(": ".ToCharArray(), StringSplitOptions.RemoveEmptyEntries).Select(m => m.ToInt32(0)).ToArray(); + + switch (msg[1]) { + // http://msdn.microsoft.com/en-us/library/aa370354(v=VS.85).aspx + case 0: //Resets progress bar and sets the expected total number of ticks in the bar. + currentTotalTicks = msg[3]; + currentProgress = 0; + if (msg.Length >= 6) { + progressDirection = msg[5] == 0 ? 1 : -1; + } + break; + case 1: + //Provides information related to progress messages to be sent by the current action. + break; + case 2: //Increments the progress bar. + if (currentTotalTicks == -1) { + break; + } + currentProgress += msg[3]*progressDirection; + break; + case 3: + //Enables an action (such as CustomAction) to add ticks to the expected total number of progress of the progress bar. + break; + } + } + + if (currentTotalTicks > 0) { + var newPercent = (currentProgress*100/currentTotalTicks); + if (actualPercent < newPercent) { + actualPercent = newPercent; + // request.Debug("Progress : {0}", newPercent); + request.Progress(_progressId, actualPercent, showMessage); + } + } + break; + } + + return MessageResult.OK; + }; + + return handler; + } + + private bool YieldPackage(InstallPackage package, string filename, Request request) { + /* + var properties = package.ExecuteStringQuery("SELECT `Property` FROM `Property` "); + foreach (var i in properties) { + Debug.WriteLine("Property {0} = {1}", i, package.Property[i]); + } + */ + if (request.YieldSoftwareIdentity(filename, package.Property["ProductName"], package.Property["ProductVersion"], "multipartnumeric", package.Property["Summary"], filename, filename, filename, Path.GetFileName(filename)) != null) { + var trusted = request.ProviderServices.IsSignedAndTrusted(filename, request); + + if (request.AddMetadata(filename, "FromTrustedSource", trusted.ToString()) == null ) { + return false; + } + + if (request.AddMetadata(filename, "ProductCode", package.Property["ProductCode"]) == null) { + return false; + } + + if (request.AddTagId(package.Property["ProductCode"].Trim(new char[] { '{', '}' })) == null) + { + return false; + } + + if (request.AddMetadata(filename, "UpgradeCode", package.Property["UpgradeCode"]) == null) { + return false; + } + + return true; + } + + return false; + } + + private bool YieldPackage(ProductInstallation package, string searchKey, Request request) { + if (request.YieldSoftwareIdentity(package.ProductCode, package.ProductName, package.ProductVersion.ToString(), "multipartnumeric", package["Summary"], package.InstallLocation, searchKey, package.InstallLocation, "?") != null) + { + if (request.AddMetadata(package.ProductCode, "ProductCode", package.ProductCode) == null) { + return false; + } + + if (request.AddTagId(package.ProductCode.Trim(new char[] {'{','}'})) == null) + { + return false; + } + + if (request.AddMetadata(package.ProductCode, "UpgradeCode", package["UpgradeCode"]) == null) { + return false; + } + return true; + } + return false; + } + } +} diff --git a/src/Microsoft.PackageManagement.MsiProvider/Properties/AssemblyInfo.cs b/src/Microsoft.PackageManagement.MsiProvider/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..ba2b1a197 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/Properties/AssemblyInfo.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. + +[assembly: AssemblyTitle("MsiProvider")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MsiProvider")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM + +[assembly: Guid("7087f372-9038-4acc-b06b-c1aeb7bd72af")] \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.MsiProvider/provider.manifest b/src/Microsoft.PackageManagement.MsiProvider/provider.manifest new file mode 100644 index 000000000..1a93161a0 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/provider.manifest @@ -0,0 +1,8 @@ + + + + diff --git a/src/Microsoft.PackageManagement.MsiProvider/resources/Microsoft.PackageManagement.Msi.Resources.Messages.resx b/src/Microsoft.PackageManagement.MsiProvider/resources/Microsoft.PackageManagement.Msi.Resources.Messages.resx new file mode 100644 index 000000000..922b76054 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsiProvider/resources/Microsoft.PackageManagement.Msi.Resources.Messages.resx @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Installing ... + + + Installing a MSI package from '{0}' + 0 - file path + + + Reboot is required to complete the installation operation. + + + Uninstalling ... + + + Uninstalling a MSI package '{0}' + 0 - package name + + + Reboot is required to complete the uninstallation operation. + + \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.MsuProvider/MsuProvider.cs b/src/Microsoft.PackageManagement.MsuProvider/MsuProvider.cs new file mode 100644 index 000000000..f980705ee --- /dev/null +++ b/src/Microsoft.PackageManagement.MsuProvider/MsuProvider.cs @@ -0,0 +1,325 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Msu.Internal { + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Management.Automation; + using Archivers.Internal.Compression.Cab; + using PackageManagement.Internal; + using PackageManagement.Internal.Implementation; + using PackageManagement.Internal.Utility.Extensions; + + public class MsuProvider { + /// + /// The name of this Package Provider + /// + internal const string ProviderName = "msu"; + + /// + /// Windows Update executable + /// + private readonly string WusaExecutableLocation = Path.Combine(Environment.SystemDirectory, "wusa.exe"); + + private static readonly Dictionary _features = new Dictionary { + {Constants.Features.SupportedExtensions, new[] {"msu"}}, + {Constants.Features.MagicSignatures, new[] {Constants.Signatures.Cab}} + }; + + /// + /// Returns the name of the Provider. + /// + /// The name of this proivder (uses the constant declared at the top of the class) + public string GetPackageProviderName() { + return ProviderName; + } + + /// + /// Performs one-time initialization of the PROVIDER. + /// + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void InitializeProvider(Request request) { + if( request == null ) { + throw new ArgumentNullException("request"); + } + + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::InitializeProvider'", ProviderName); + } + + /// + /// Returns a collection of strings to the client advertizing features this provider supports. + /// + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void GetFeatures(Request request) { + if( request == null ) { + throw new ArgumentNullException("request"); + } + + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::GetFeatures' ", ProviderName); + foreach (var feature in _features) { + request.Yield(feature); + } + } + + /// + /// Returns dynamic option definitions to the HOST + /// + /// The category of dynamic options that the HOST is interested in + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void GetDynamicOptions(string category, Request request) { + if( request == null ) { + throw new ArgumentNullException("request"); + } + + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::GetDynamicOptions' '{1}'", ProviderName, category); + + switch ((category ?? string.Empty).ToLowerInvariant()) { + case "install": + // options required for install/uninstall/getinstalledpackages + break; + + case "provider": + // options used with this provider. Not currently used. + break; + + case "source": + // options for package sources + break; + + case "package": + // options used when searching for packages + break; + } + } + + /// + /// Finds packages given a locally-accessible filename + /// Package information must be returned using request.YieldPackage(...) function. + /// + /// the full path to the file to determine if it is a package + /// + /// if this is greater than zero (and the number should have been generated using StartFind(...), + /// the core is calling this multiple times to do a batch search request. The operation can be delayed until + /// CompleteFind(...) is called + /// + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void FindPackageByFile(string file, int id, Request request) { + if( request == null ) { + throw new ArgumentNullException("request"); + } + if( string.IsNullOrWhiteSpace(file) ) { + throw new ArgumentNullException("file"); + } + + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::FindPackageByFile' '{1}','{2}'", ProviderName, file, id); + + if (file.FileExists()) + { + var info = new CabInfo(file); + + request.YieldSoftwareIdentity(file, info.Name, null, null, null, null, null, file, info.Name); + + var files = info.GetFiles(); + foreach (var i in files) { + // read the properties file + if (i.FullNameExtension == ".txt") + { + request.Debug("Reading properties file {0}", i.FullName); + using (var reader = i.OpenText()) + { + var contents = reader.ReadToEnd(); + Dictionary keyValuePairs = contents.Split('\n').Select(line => line.Split('=')).Where(v => v.Count() == 2).ToDictionary(pair => pair[0], pair => pair[1]); + + foreach (var pair in keyValuePairs) + { + request.AddMetadata(pair.Key.Replace(' ', '_'), pair.Value.Replace("\"", "").Replace("\r", "")); + } + } + } + } + } + } + + /// + /// + /// + /// + /// + /// + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void GetInstalledPackages(string name, string requiredVersion, string minimumVersion, string maximumVersion, Request request) + { + if( request == null ) { + throw new ArgumentNullException("request"); + } + + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::GetInstalledPackages' '{1}'", ProviderName, name); + + using (PowerShell ps = PowerShell.Create()) + { + ps.AddScript(@"$updateSession = new-object -com Microsoft.Update.Session + $updateSearcher = $updateSession.CreateUpdateSearcher() + $updateSearcher.queryhistory(1, $updateSearcher.GetTotalHistoryCount()) | select Title, SupportUrl, Date, ResultCode, Description"); + var output = ps.Invoke(); + + var wildcardPattern = new WildcardPattern(name, WildcardOptions.CultureInvariant | WildcardOptions.IgnoreCase); + + foreach (var obj in output) + { + if (obj != null) + { + var title = obj.Properties["Title"] != null ? obj.Properties["Title"].Value as string : null; + + if (title != null && (string.IsNullOrWhiteSpace(name) || wildcardPattern.IsMatch(title))) + { + var supportUrl = obj.Properties["SupportUrl"] != null ? obj.Properties["SupportUrl"].Value as string : null; + var date = obj.Properties["Date"] != null ? obj.Properties["Date"].Value as DateTime? : null; + var resultCode = obj.Properties["ResultCode"] != null ? obj.Properties["ResultCode"].Value as int? : null; + var description = obj.Properties["Description"] != null ? obj.Properties["Description"].Value as string : null; + + YieldPackage(name, request, title, supportUrl, date, resultCode, description); + } + } + } + } + } + + /// + /// Installs a given package. + /// + /// A provider supplied identifier that specifies an exact package + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void InstallPackage(string fastPackageReference, Request request) { + if( request == null ) { + throw new ArgumentNullException("request"); + } + if( string.IsNullOrWhiteSpace(fastPackageReference) ) { + throw new ArgumentNullException("fastPackageReference"); + } + + string errorLogFolder = Path.GetTempPath() + Guid.NewGuid(); + DirectoryInfo errorDir = Directory.CreateDirectory(errorLogFolder); + string errorLogPath = errorLogFolder + "\\msuLog.evtx"; + + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::InstallPackage' '{1}'", ProviderName, fastPackageReference); + + string output; + string args = "\"" + fastPackageReference + "\"" + " /quiet /norestart /log:" + "\"" + errorLogPath + "\""; + int exitCode = request.ProviderServices.StartProcess(WusaExecutableLocation, args, true, out output, request); + if (exitCode == 0) + { + request.Verbose("Provider '{0}', Package '{1}': Installation succeeded", ProviderName, fastPackageReference); + } + else if (exitCode == 3010) //Exit code: 3010 (0xBC2) ERROR_SUCCESS_REBOOT_REQUIRED + { + request.Warning(Resources.Messages.InstallRequiresReboot); + } + else + { + request.Error(Microsoft.PackageManagement.Internal.ErrorCategory.InvalidOperation, fastPackageReference, Resources.Messages.InstallFailed, fastPackageReference, String.Format(CultureInfo.CurrentCulture, "0x{0:X}", exitCode), errorLogPath); + } + + try { + if (exitCode == 0 || exitCode == 3010) + { + if (errorDir.Exists) + errorDir.Delete(true); + } + } catch { + } + + } + + /// + /// Uninstalls a package + /// + /// + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + public void UninstallPackage(string fastPackageReference, Request request) { + if( request == null ) { + throw new ArgumentNullException("request"); + } + if( string.IsNullOrWhiteSpace(fastPackageReference) ) { + throw new ArgumentNullException("fastPackageReference"); + } + + // Nice-to-have put a debug message in that tells what's going on. + request.Debug("Calling '{0}::UninstallPackage' '{1}'", ProviderName, fastPackageReference); + } + + /// + /// Yields package information to OneGet Core + /// + /// + /// + /// + /// + /// + /// + /// + /// Whether operation succeeded or was interrupted + private bool YieldPackage(string searchKey, Request request, string title, string supportUrl, DateTime? date, int? resultCode, string description) + { + if (request.YieldSoftwareIdentity(title, title, null, null, description, null, searchKey, "?", "?") != null) + { + if (request.AddMetadata("SupportUrl", supportUrl) == null) + { + return false; + } + + if (date != null && request.AddMetadata("Date", ((DateTime)date).ToString(CultureInfo.CurrentCulture)) == null) + { + return false; + } + + if (resultCode != null && request.AddMetadata("ResultCode", resultCode.ToString()) == null) + { + return false; + } + return true; + } + return false; + } + } +} diff --git a/src/Microsoft.PackageManagement.MsuProvider/Properties/AssemblyInfo.cs b/src/Microsoft.PackageManagement.MsuProvider/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..f2b9c16b9 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsuProvider/Properties/AssemblyInfo.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("MsuProvider")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MsuProvider")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("758b6eb4-cf8d-4841-bdce-ae002bd207f8")] \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.MsuProvider/provider.manifest b/src/Microsoft.PackageManagement.MsuProvider/provider.manifest new file mode 100644 index 000000000..53a3da77a --- /dev/null +++ b/src/Microsoft.PackageManagement.MsuProvider/provider.manifest @@ -0,0 +1,8 @@ + + + + diff --git a/src/Microsoft.PackageManagement.MsuProvider/resources/Microsoft.PackageManagement.Msu.Resources.Messages.resx b/src/Microsoft.PackageManagement.MsuProvider/resources/Microsoft.PackageManagement.Msu.Resources.Messages.resx new file mode 100644 index 000000000..43a0b40d6 --- /dev/null +++ b/src/Microsoft.PackageManagement.MsuProvider/resources/Microsoft.PackageManagement.Msu.Resources.Messages.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Package '{0}': Installation failed with Windows Update error code '{1}'. Please look at '{2}' for details. + + + Reboot is required to complete the installation operation. + + \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.NuGetProvider/NuGetExtensions.cs b/src/Microsoft.PackageManagement.NuGetProvider/NuGetExtensions.cs new file mode 100644 index 000000000..0a97cb101 --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/NuGetExtensions.cs @@ -0,0 +1,39 @@ +using Microsoft.PackageManagement.Provider.Utility; + +namespace Microsoft.PackageManagement.NuGetProvider +{ + using System; + using System.Globalization; + + internal static class StringExtensions + { + + internal static string MakeFindPackageByIdQuery(this string packageId, string urlFormat) + { + return String.Format(CultureInfo.InvariantCulture, urlFormat, packageId); + } + + internal static string MakeSearchQuery(this string searchTerm, string baseUrl, bool allowPrereleaseVersions, bool allVersions) + { + // Fill in the searchTerm, targetFrameworks, and allowPrereleaseVersions fileds in the query format string. + // Skip and Take will be filled later on + var newSearchTerm = string.Format(CultureInfo.InvariantCulture, NuGetConstant.SearchTerm, searchTerm, allowPrereleaseVersions ? "true" : "false"); + + // Make the uri query string + return PathUtility.UriCombine(baseUrl, String.Concat((allVersions ? NuGetConstant.SearchFilterAllersions : NuGetConstant.SearchFilter), newSearchTerm)); + } + + internal static string InsertSkipAndTop(this string query) + { + return String.Concat(query, NuGetConstant.SkipAndTop); + } + } + internal static class ExceptionExtensions + { + internal static void Dump(this Exception ex, NuGetRequest request) + { + request.Debug(ex.ToString()); + } + } + +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.NuGetProvider/NugetLightClient.cs b/src/Microsoft.PackageManagement.NuGetProvider/NugetLightClient.cs new file mode 100644 index 000000000..70c3fcdc9 --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/NugetLightClient.cs @@ -0,0 +1,1154 @@ +namespace Microsoft.PackageManagement.NuGetProvider +{ + using Resources; + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Net.Http; + using System.Threading.Tasks; + using System.Linq; + using System.IO.Compression; + using System.Net; + using System.Security.Cryptography; + using System.Threading; + using Microsoft.PackageManagement.Provider.Utility; + using Microsoft.PackageManagement.NuGetProvider.Utility; + + /// + /// Utility to handle the Find, Install, Uninstall-Package etc operations. + /// + internal static class NuGetClient + { + /// + /// Find the package via the given uri query. + /// + /// A full Uri. A sample Uri looks like "http://www.nuget.org/api/v2/FindPackagesById()?id='Jquery'" + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + /// Package objects + internal static IEnumerable FindPackage(string query, NuGetRequest request) { + request.Debug(Messages.DebugInfoCallMethod, "NuGetClient", "FindPackage"); + + request.Verbose(Messages.SearchingRepository, query, ""); + + return HttpClientPackageRepository.SendRequest(query, request); + } + + /// + /// Download a package that matches the given version and name and install it on the local system. + /// + /// Package name + /// Package version + /// An object passed in from the PackageManagement platform that contains APIs that can be used to interact with it + /// Package source + /// Full uri + /// the hash of the package + /// the hash algorithm of the package + /// progress tracker to help keep track of progressid, start and end of the progress + /// PackageItem object + internal static PackageItem InstallPackage( + string packageName, + string version, + NuGetRequest request, + PackageSource source, + string queryUrl, + string packageHash, + string packageHashAlgorithm, + ProgressTracker progressTracker + ) + { + request.Debug(Messages.DebugInfoCallMethod, "NuGetClient", "InstallPackage"); + + //If the destination folder does not exists, create it + string destinationPath = request.Destination; + request.Verbose(string.Format(CultureInfo.InvariantCulture, "InstallPackage' - name='{0}', version='{1}',destination='{2}'", packageName, version, destinationPath)); + + string directoryToDeleteWhenFailed = string.Empty; + bool needToDelete = false; + string installFullPath = string.Empty; + + try + { + if (!Directory.Exists(destinationPath)) { + Directory.CreateDirectory(destinationPath); + // delete the destinationPath later on if we fail to install and if destinationPath did not exist before + directoryToDeleteWhenFailed = destinationPath; + } + + //Create a folder under the destination path to hold the package + string installDir = FileUtility.MakePackageDirectoryName(request.ExcludeVersion.Value, destinationPath, packageName, version); + + if (!Directory.Exists(installDir)) { + Directory.CreateDirectory(installDir); + + // if directoryToDeleteWhenFailed is null then the destinationPath already exists before so we should not delete it + if (String.IsNullOrWhiteSpace(directoryToDeleteWhenFailed)) + { + directoryToDeleteWhenFailed = installDir; + } + } + + //Get the package file name based on the version and id + string fileName = FileUtility.MakePackageFileName(request.ExcludeVersion.Value, packageName, version, NuGetConstant.PackageExtension); + + installFullPath = Path.Combine(installDir, fileName); + + // we assume downloading takes 70% of the progress + int endProgressDownloading = progressTracker.ConvertPercentToProgress(0.7); + + //download to fetch the package + DownloadPackage(packageName, version, installFullPath, queryUrl, request, source, new ProgressTracker(progressTracker.ProgressID, progressTracker.StartPercent, endProgressDownloading)); + + // check that we have the file + if (!File.Exists(installFullPath)) + { + needToDelete = true; + // error message is package failed to be downloaded + request.WriteError(ErrorCategory.ResourceUnavailable, installFullPath, Constants.Messages.PackageFailedInstallOrDownload, packageName, + CultureInfo.CurrentCulture.TextInfo.ToLower(Constants.Download)); + return null; + } + + #region verify hash + //we don't enable checking for hash here because it seems like nuget provider does not + //checks that there is hash. Otherwise we don't carry out the install + + if (string.IsNullOrWhiteSpace(packageHash)) + { + // if no hash (for example, vsts feed, install the package but log verbose message) + request.Verbose(string.Format(CultureInfo.CurrentCulture, Resources.Messages.HashNotFound, packageName)); + //parse the package + var pkgItem = InstallPackageLocal(packageName, version, request, source, installFullPath, new ProgressTracker(progressTracker.ProgressID, endProgressDownloading, progressTracker.EndPercent)); + return pkgItem; + } + + // Verify the hash + using (FileStream stream = File.OpenRead(installFullPath)) + { + HashAlgorithm hashAlgorithm = null; + + switch (packageHashAlgorithm == null ? string.Empty : packageHashAlgorithm.ToLowerInvariant()) + { + case "sha256": + hashAlgorithm = SHA256.Create(); + break; + + case "md5": + hashAlgorithm = MD5.Create(); + break; + + case "sha512": + // Flows to default case + + // default to sha512 algorithm + default: + hashAlgorithm = SHA512.Create(); + break; + } + + if (hashAlgorithm == null) + { + // delete the file downloaded. VIRUS!!! + needToDelete = true; + request.WriteError(ErrorCategory.SecurityError, packageHashAlgorithm, Constants.Messages.HashNotSupported, packageHashAlgorithm); + return null; + } + + // compute the hash + byte[] computedHash = hashAlgorithm.ComputeHash(stream); + + // convert the original hash we got from the feed + byte[] downloadedHash = Convert.FromBase64String(packageHash); + + // if they are not equal, just issue out verbose because there is a current bug in backend + // where editing the published module will result in a package with a different hash than the one + // provided on the feed + if (!Enumerable.SequenceEqual(computedHash, downloadedHash)) + { + // delete the file downloaded. VIRUS!!! + request.Verbose(Constants.Messages.HashNotMatch, packageName); + } + + //parse the package + var pkgItem = InstallPackageLocal(packageName, version, request, source, installFullPath, new ProgressTracker(progressTracker.ProgressID, endProgressDownloading, progressTracker.EndPercent)); + return pkgItem; + } + + #endregion + + } + catch (Exception ex) + { + // the error will be package "packageName" failed to install because : "reason" + ex.Dump(request); + request.WriteError(ErrorCategory.InvalidResult, packageName, Resources.Messages.PackageFailedToInstallReason, packageName, ex.Message); + needToDelete = true; + } + finally + { + if (needToDelete) + { + // if the directory exists just delete it because it will contains the file as well + if (!String.IsNullOrWhiteSpace(directoryToDeleteWhenFailed) && Directory.Exists(directoryToDeleteWhenFailed)) + { + try + { + FileUtility.DeleteDirectory(directoryToDeleteWhenFailed, true, isThrow: false); + } + catch { } + } + + // if for some reason, we can't delete the directory or if we don't need to delete the directory + // then we have to delete installFullPath + if (File.Exists(installFullPath)) + { + FileUtility.DeleteFile(installFullPath, isThrow: false); + } + } + } + + return null; + } + + /// + /// Install a single package without checking for dependencies + /// + /// + /// + /// + /// + internal static bool InstallSinglePackage(PackageItem pkgItem, NuGetRequest request, ProgressTracker progressTracker) + { + PackageItem packageToBeInstalled; + + if (pkgItem == null || pkgItem.PackageSource == null || pkgItem.PackageSource.Repository == null) + { + return false; + } + + // If the source location exists as a directory then we try to get the file location and provide to the packagelocal + if (Directory.Exists(pkgItem.PackageSource.Location)) + { + var fileLocation = pkgItem.PackageSource.Repository.FindPackage(pkgItem.Id, new SemanticVersion(pkgItem.Version), request).FullFilePath; + packageToBeInstalled = NuGetClient.InstallPackageLocal(pkgItem.Id, pkgItem.Version, request, pkgItem.PackageSource, fileLocation, progressTracker); + } + else + { + //V2 download package protocol: + //sample url: http://www.nuget.org/api/v2/package/jQuery/2.1.3 + string append = String.Format(CultureInfo.InvariantCulture, "/package/{0}/{1}", pkgItem.Id, pkgItem.Version); + string httpquery = PathUtility.UriCombine(pkgItem.PackageSource.Repository.Source, append); + + // wait for the result from installpackage + packageToBeInstalled = NuGetClient.InstallPackage(pkgItem.Id, pkgItem.Version, request, pkgItem.PackageSource, + string.IsNullOrWhiteSpace(pkgItem.Package.ContentSrcUrl) ? httpquery : pkgItem.Package.ContentSrcUrl, + pkgItem.Package.PackageHash, pkgItem.Package.PackageHashAlgorithm, progressTracker); + } + + // Package is installed successfully + if (packageToBeInstalled != null) + { + // if this is a http repository, return metadata from online + if (!pkgItem.PackageSource.Repository.IsFile) + { + request.YieldPackage(pkgItem, packageToBeInstalled.PackageSource.Name, packageToBeInstalled.FullPath); + } + else + { + request.YieldPackage(packageToBeInstalled, packageToBeInstalled.PackageSource.Name, packageToBeInstalled.FullPath); + } + + request.Debug(Messages.DebugInfoReturnCall, "NuGetClient", "InstallSinglePackage"); + return true; + } + + return false; + } + + /// + /// Download a single package to destination without checking for dependencies + /// + /// + /// + /// + /// + /// + internal static bool DownloadSinglePackage(PackageItem pkgItem, NuGetRequest request, string destLocation, ProgressTracker progressTracker) + { + if (string.IsNullOrWhiteSpace(pkgItem.PackageFilename) || pkgItem.PackageSource == null || pkgItem.PackageSource.Location == null + || (pkgItem.PackageSource.IsSourceAFile && pkgItem.Package == null)) + { + request.WriteError(ErrorCategory.ObjectNotFound, pkgItem.Id, Constants.Messages.UnableToResolvePackage, pkgItem.Id); + return false; + } + + // this is if the user says -force + bool force = request.GetOptionValue("Force") != null; + + // combine the path and the file name + destLocation = Path.Combine(destLocation, pkgItem.PackageFilename); + + // if the file already exists + if (File.Exists(destLocation)) + { + // if no force, just return + if (!force) + { + request.Verbose(Constants.Messages.SkippedDownloadedPackage, pkgItem.Id); + request.YieldPackage(pkgItem, pkgItem.PackageSource.Name); + return true; + } + + // here we know it is forced, so delete + FileUtility.DeleteFile(destLocation, isThrow: false); + + // if after we try delete, it is still there, tells the user we can't perform the action + if (File.Exists(destLocation)) + { + request.WriteError(ErrorCategory.ResourceUnavailable, destLocation, Constants.Messages.UnableToOverwriteExistingFile, destLocation); + return false; + } + } + + bool downloadSuccessful = false; + + try + { + // if no repository, we can't do anything + if (pkgItem.PackageSource.Repository == null) + { + return false; + } + + if (pkgItem.PackageSource.Repository.IsFile) + { + using (var input = File.OpenRead(pkgItem.Package.FullFilePath)) + { + using (var output = new FileStream(destLocation, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + input.CopyTo(output); + } + } + } + else + { + //V2 download package protocol: + //sample url: http://www.nuget.org/api/v2/package/jQuery/2.1.3 + string append = String.Format(CultureInfo.InvariantCulture, "/package/{0}/{1}", pkgItem.Id, pkgItem.Version); + string httpquery = PathUtility.UriCombine(pkgItem.PackageSource.Repository.Source, append); + + downloadSuccessful = NuGetClient.DownloadPackage(pkgItem.Id, pkgItem.Version, destLocation, + string.IsNullOrWhiteSpace(pkgItem.Package.ContentSrcUrl) ? httpquery : pkgItem.Package.ContentSrcUrl, request, pkgItem.PackageSource, progressTracker); + } + } + catch (Exception ex) + { + ex.Dump(request); + return false; + } + + if (downloadSuccessful) + { + request.Verbose(Resources.Messages.SuccessfullyDownloaded, pkgItem.Id); + // provide the directory we save to to yieldpackage + request.YieldPackage(pkgItem, pkgItem.PackageSource.Name, Path.GetDirectoryName(destLocation)); + return true; + } + + return false; + } + + /// + /// Install a single package. Also install any of its dependency if they are available (the dependency will be installed first). + /// For dependencies, we will only get those that are not installed. + /// Operation is either install or download + /// installOrDownloadFunction is a function that takes in a packageitem and performs either install or download on it + /// + /// + /// + /// + /// + /// + internal static bool InstallOrDownloadPackageHelper(PackageItem pkgItem, NuGetRequest request, string operation, + Func installOrDownloadFunction) + { + // pkgItem.Sources is the source that the user input. The request will try this source. + request.OriginalSources = pkgItem.Sources; + + bool hasDependencyLoop = false; + + // Get the dependencies that are not already installed + var dependencies = NuGetClient.GetPackageDependenciesToInstall(request, pkgItem, ref hasDependencyLoop).ToArray(); + + // If there is a dependency loop. Warn the user and don't install the package + if (hasDependencyLoop) + { + // package itself didn't install. Report error + request.WriteError(ErrorCategory.DeadlockDetected, pkgItem.Id, Constants.Messages.DependencyLoopDetected, pkgItem.Id); + return false; + } + + // request may get canceled if there is a package dependencies missing + if (request.IsCanceled) + { + return false; + } + + int n = 0; + int numberOfDependencies = dependencies.Count(); + + // Start progress + ProgressTracker progressTracker = ProgressTracker.StartProgress(null, string.Format(CultureInfo.InvariantCulture, Messages.InstallingOrDownloadingPackage, operation, pkgItem.Id), request); + + try + { + // check that this package has dependency and the user didn't want to skip dependencies + if (numberOfDependencies > 0) + { + // let's install dependencies + foreach (var dep in dependencies) + { + request.Progress(progressTracker.ProgressID, (n * 100 / (numberOfDependencies + 1)), string.Format(CultureInfo.InvariantCulture, Messages.InstallingOrDownloadingDependencyPackage, operation, dep.Id)); + + // start a subprogress bar for the dependent package + ProgressTracker subProgressTracker = ProgressTracker.StartProgress(progressTracker, string.Format(CultureInfo.InvariantCulture, Messages.InstallingOrDownloadingPackage, operation, dep.Id), request); + try + { + // Check that we successfully installed the dependency + if (!installOrDownloadFunction(dep, subProgressTracker)) + { + request.WriteError(ErrorCategory.InvalidResult, dep.Id, Constants.Messages.DependentPackageFailedInstallOrDownload, dep.Id, CultureInfo.CurrentCulture.TextInfo.ToLower(operation)); + return false; + } + } + finally + { + request.CompleteProgress(subProgressTracker.ProgressID, true); + } + + n++; + request.Progress(progressTracker.ProgressID, (n * 100 / (numberOfDependencies + 1)), string.Format(CultureInfo.InvariantCulture, Messages.InstalledOrDownloadedDependencyPackage, operation, dep.Id)); + } + } + + // Now let's install the main package + // the start progress should be where we finished installing the dependencies + if (installOrDownloadFunction(pkgItem, new ProgressTracker(progressTracker.ProgressID, (n * 100 / (numberOfDependencies + 1)), 100))) + { + return true; + } + } + catch (Exception ex) + { + ex.Dump(request); + } + finally + { + // Report that we have completed installing the package and its dependency this does not mean there are no errors. + // Just that it's completed. + request.CompleteProgress(progressTracker.ProgressID, true); + } + + // package itself didn't install. Report error + request.WriteError(ErrorCategory.InvalidResult, pkgItem.Id, Constants.Messages.PackageFailedInstallOrDownload, pkgItem.Id, CultureInfo.CurrentCulture.TextInfo.ToLower(operation)); + + return false; + } + + /// + /// Get the package dependencies that we need to installed. hasDependencyLoop is set to true if dependencyloop is detected. + /// + /// + /// + /// + /// + internal static IEnumerable GetPackageDependenciesToInstall(NuGetRequest request, PackageItem packageItem, ref bool hasDependencyLoop) + { + request.Debug(Messages.DebugInfoCallMethod, "NuGetClient", "GetPackageDependencies"); + + // No dependency + if (packageItem.Package == null || packageItem.Package.DependencySetList == null) + { + request.Debug(Messages.DebugInfoReturnCall, "NuGetClient", "GetPackageDependencies"); + return Enumerable.Empty(); + } + + // Returns list of dependency to be installed in the correct order that we should install them + List dependencyToBeInstalled = new List(); + + HashSet permanentlyMarked = new HashSet(new PackageItemComparer()); + HashSet temporarilyMarked = new HashSet(new PackageItemComparer()); + + // checks that there are no dependency loop + hasDependencyLoop = !DepthFirstVisit(packageItem, temporarilyMarked, permanentlyMarked, dependencyToBeInstalled, request); + + if (!hasDependencyLoop) + { + request.Debug(Messages.DebugInfoReturnCall, "NuGetClient", "GetPackageDependencies"); + // remove the last item of the list because that is the package itself + dependencyToBeInstalled.RemoveAt(dependencyToBeInstalled.Count - 1); + return dependencyToBeInstalled; + } + + // there are dependency loop. + request.Debug(Messages.DebugInfoReturnCall, "NuGetClient", "GetPackageDependencies"); + return Enumerable.Empty(); + } + + /// + /// Do a dfs visit. returns false if a cycle is encountered. Add the packageItem to the list at the end of each visit + /// + /// + /// + /// + /// + /// + /// + internal static bool DepthFirstVisit(PackageItem packageItem, HashSet temporarilyMarked, HashSet permanentlyMarked, List dependencyToBeInstalled, NuGetRequest request) + { + // dependency loop detected because the element is temporarily marked + if (temporarilyMarked.Contains(packageItem)) + { + return false; + } + + // this is permanently marked. So we don't have to visit it. + // This is to resolve a case where we have: A->B->C and A->C. Then we need this when we visit C again from either B or A. + if (permanentlyMarked.Contains(packageItem)) + { + return true; + } + + // Mark this node temporarily so we can detect cycle. + temporarilyMarked.Add(packageItem); + + // Visit the dependency + foreach (var dependency in GetPackageDependenciesHelper(packageItem, request)) + { + if (!DepthFirstVisit(dependency, temporarilyMarked, permanentlyMarked, dependencyToBeInstalled, request)) + { + // if dfs returns false then we have encountered a loop + return false; + } + // otherwise visit the next dependency + } + + // Add the package to the list so we can install later + dependencyToBeInstalled.Add(packageItem); + + // Done with this node so mark it permanently + permanentlyMarked.Add(packageItem); + + // Unmark it temporarily + temporarilyMarked.Remove(packageItem); + + return true; + } + + /// + /// Returns the package dependencies of packageItem. We only return the dependencies that are not installed in the destination folder of request + /// + /// + /// + private static IEnumerable GetPackageDependenciesHelper(PackageItem packageItem, NuGetRequest request) + { + if (packageItem.Package.DependencySetList == null) + { + yield break; + } + + bool force = request.GetOptionValue("Force") != null; + foreach (var depSet in packageItem.Package.DependencySetList) + { + if (depSet.Dependencies == null) + { + continue; + } + + foreach (var dep in depSet.Dependencies) + { + // Get the min dependencies version + string minVersion = dep.DependencyVersion.MinVersion.ToStringSafe(); + + // Get the max dependencies version + string maxVersion = dep.DependencyVersion.MaxVersion.ToStringSafe(); + + if (!force) + { + bool installed = false; + + var installedPackages = request.InstalledPackages.Value; + + if (request.InstalledPackages.Value.Count() > 0) + { + // check the installedpackages options passed in + foreach (var installedPackage in request.InstalledPackages.Value) + { + // if name not match, move on to the next entry + if (!string.Equals(installedPackage.Id, dep.Id, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // if no version and if name matches, skip + if (string.IsNullOrWhiteSpace(installedPackage.Version)) + { + // skip this dependency + installed = true; + break; + } + + SemanticVersion packageVersion = new SemanticVersion(installedPackage.Version); + + // checks min and max + if (request.MinAndMaxVersionMatched(packageVersion, minVersion, maxVersion, dep.DependencyVersion.IsMinInclusive, dep.DependencyVersion.IsMaxInclusive)) + { + // skip this dependency + installed = true; + break; + } + } + } + // check whether package is installed at destination. only used this option if installedpackages not passed in + else if (request.GetInstalledPackages(dep.Id, null, minVersion, maxVersion, minInclusive: dep.DependencyVersion.IsMinInclusive, maxInclusive: dep.DependencyVersion.IsMaxInclusive, terminateFirstFound: true)) + { + installed = true; + } + + if (installed) + { + request.Verbose(String.Format(CultureInfo.CurrentCulture, Messages.AlreadyInstalled, dep.Id)); + // already have a dependency so move on + continue; + } + } + + // get all the packages that match this dependency + var dependentPackageItem = request.GetPackageById(dep.Id, request, minimumVersion: minVersion, maximumVersion: maxVersion, minInclusive: dep.DependencyVersion.IsMinInclusive, maxInclusive: dep.DependencyVersion.IsMaxInclusive).ToArray(); + + if (dependentPackageItem.Length == 0) + { + request.WriteError(ErrorCategory.ObjectNotFound, dep.Id, Constants.Messages.UnableToFindDependencyPackage, dep.Id); + + break; + } + + // Get the package that is the latest version + yield return dependentPackageItem.OrderByDescending(each => each.Version).FirstOrDefault(); + } + } + } + + /// + /// Download a package from a file repository that matches the given version and name and install it on the local system. + /// + /// Package name + /// Package version + /// An object passed in from the PackageManagement platform that contains APIs that can be used to interact with it + /// Package source + /// File source path pointing to the package to be installed + /// progress tracker to help keep track of progressid, start and end of the progress + /// PackageItem object + internal static PackageItem InstallPackageLocal( + string packageName, + string version, + NuGetRequest request, + PackageSource source, + string sourceFilePath, + ProgressTracker progressTracker + ) + { + request.Debug(Messages.DebugInfoCallMethod, "NuGetClient", "InstallPackageLocal"); + + string tempSourceFilePath = null; + string tempSourceDirectory = null; + + string directoryToDeleteWhenFailed = String.Empty; + bool needToDelete = false; + + try + { + string destinationFilePath = request.Destination; + request.Verbose(string.Format(CultureInfo.InvariantCulture, "InstallPackageLocal' - name='{0}', version='{1}',destination='{2}'", packageName, version, destinationFilePath)); + request.Debug(sourceFilePath); + + if (string.IsNullOrWhiteSpace(sourceFilePath)) + { + throw new ArgumentNullException(sourceFilePath); + } + + if (!File.Exists(sourceFilePath)) { + throw new FileNotFoundException(sourceFilePath); + } + + //Create the destination directory if it does not exist + if (!Directory.Exists(destinationFilePath)) { + Directory.CreateDirectory(destinationFilePath); + directoryToDeleteWhenFailed = destinationFilePath; + } + + //Make a temp folder in the user appdata temp directory + tempSourceFilePath = FileUtility.GetTempFileFullPath(fileExtension: NuGetConstant.PackageExtension); + + //Copy over the source file from the folder repository to the temp folder + File.Copy(sourceFilePath, tempSourceFilePath, true); + + request.Progress(progressTracker.ProgressID, progressTracker.StartPercent, string.Format(CultureInfo.CurrentCulture, Messages.Unzipping)); + //Unzip it + tempSourceDirectory = PackageUtility.DecompressFile(tempSourceFilePath); + + //Get a package directory under the destination path to store the package + string installedFolder = FileUtility.MakePackageDirectoryName(request.ExcludeVersion.Value, destinationFilePath, packageName, version); + + // if we did not set the directory before, then the destinationFilePath already exists, so we should not delete it + if (string.IsNullOrWhiteSpace(directoryToDeleteWhenFailed)) + { + directoryToDeleteWhenFailed = installedFolder; + } + + //File folder format of the Nuget packages looks like the following after installed: + //Jquery.2.0.1 + // - JQuery.2.0.1.nupkg + // - contents and other stuff + + // unzipping should take most of the time (assuming 70%) + + request.Progress(progressTracker.ProgressID, progressTracker.ConvertPercentToProgress(0.7), string.Format(CultureInfo.CurrentCulture, Messages.CopyUnzippedFiles, installedFolder)); + + //Copy the unzipped files to under the package installed folder + FileUtility.CopyDirectory(tempSourceDirectory, installedFolder, true); + + // copying should take another 15% + // copy the nupkg file if it's not in + var nupkgFilePath = Path.Combine(installedFolder, FileUtility.MakePackageFileName(request.ExcludeVersion.Value, packageName, version, NuGetConstant.PackageExtension)); + + if (!File.Exists(nupkgFilePath)) + { + File.Copy(sourceFilePath, nupkgFilePath); + } + + request.Progress(progressTracker.ProgressID, progressTracker.ConvertPercentToProgress(0.85), string.Format(CultureInfo.CurrentCulture, Messages.ReadingManifest)); + + //Read the package manifest and return the package object + string nuspec = Path.Combine(installedFolder, packageName) + NuGetConstant.ManifestExtension; + + PackageBase package = PackageUtility.ProcessNuspec(nuspec); + + var pkgItem = new PackageItem { + Package = package, + PackageSource = source, + FastPath = request.MakeFastPath(source, package.Id, package.Version), + FullPath = installedFolder + }; + + // Delete the nuspec file + //Get a package file path + var nuspecFilePath = Path.Combine(installedFolder, packageName + NuGetConstant.ManifestExtension); + + if (File.Exists(nuspecFilePath)) + { + FileUtility.DeleteFile(nuspecFilePath, false); + } + + request.Debug(Messages.DebugInfoReturnCall, "NuGetClient", "InstallPackageLocal"); + + request.Progress(progressTracker.ProgressID, progressTracker.EndPercent, string.Format(CultureInfo.CurrentCulture, Messages.FinishInstalling, packageName)); + + return pkgItem; + + } catch (Exception ex) { + needToDelete = true; + // the error will be package "packageName" failed to install because : "reason" + ex.Dump(request); + request.WriteError(ErrorCategory.InvalidResult, packageName, Resources.Messages.PackageFailedToInstallReason, packageName, ex.Message); + throw; + } finally { + if (needToDelete && Directory.Exists(directoryToDeleteWhenFailed)) + { + FileUtility.DeleteDirectory(directoryToDeleteWhenFailed, true, isThrow: false); + } + + FileUtility.DeleteFile(tempSourceFilePath, isThrow:false); + FileUtility.DeleteDirectory(tempSourceDirectory, recursive: true, isThrow: false); + } + } + + /// + /// Perform package uninstallation. + /// + /// Object given by the PackageManagement platform + /// PackageItem object + internal static void UninstallPackage(NuGetRequest request, PackageItem pkg) + { + request.Debug(Messages.DebugInfoCallMethod, "NuGetClient", "UninstallPackage"); + + if (pkg == null) + { + throw new ArgumentNullException(paramName: "pkg"); + } + + var dir = pkg.InstalledDirectory; + + if (String.IsNullOrWhiteSpace(dir) || !Directory.Exists(dir)) + { + return; + } + + FileUtility.DeleteDirectory(pkg.InstalledDirectory, recursive:true, isThrow:false); + + //Inform a user which package is deleted via the packageManagement platform + request.Verbose(Messages.UninstalledPackage, "NuGetClient", pkg.Id); + + request.YieldPackage(pkg, pkg.Id); + } + + /// + /// Download a nuget package. + /// + /// Package name + /// Package version + /// Destination location to store the downloaded package + /// Uri to query the package + /// An object passed in from the PackageManagement platform that contains APIs that can be used to interact with it + /// source to download the package + /// Utility class to help track progress + /// + internal static bool DownloadPackage(string packageName, string version, string destination, string queryUrl, NuGetRequest request, PackageSource pkgSource, ProgressTracker progressTracker) + { + try { + request.Verbose(string.Format(CultureInfo.InvariantCulture, "DownloadPackage' - name='{0}', version='{1}',destination='{2}', uri='{3}'", packageName, version, destination, queryUrl)); + + if (new Uri(queryUrl).IsFile) { + throw new ArgumentException(Constants.Messages.UriSchemeNotSupported, queryUrl); + } + + long result = 0; + + // Do not need to validate here again because the job is done by the httprepository that supplies the queryurl + //Downloading the package + //request.Verbose(httpquery); + result = DownloadDataToFileAsync(destination, queryUrl, request, PathUtility.GetNetworkCredential(request.CredentialUsername, request.CredentialPassword), progressTracker).Result; + + if (result == 0 || !File.Exists(destination)) + { + request.Verbose(Messages.FailedDownloadPackage, packageName, queryUrl); + request.Warning(Constants.Messages.SourceLocationNotValid, queryUrl); + return false; + } else { + request.Verbose(Messages.CompletedDownload, packageName); + return true; + } + } catch (Exception ex) { + ex.Dump(request); + request.Warning(Constants.Messages.PackageFailedInstallOrDownload, packageName, CultureInfo.CurrentCulture.TextInfo.ToLower(Constants.Download)); + throw; + } + } + + /// + /// Returns the appropriate stream depending on the encoding + /// + /// + /// + private static Stream GetStreamBasedOnEncoding(HttpResponseMessage response) + { + Stream result = response.Content.ReadAsStreamAsync().Result; + // Gzip encoding so returns gzip stream + if (response.Content.Headers.ContentEncoding.Contains("gzip")) + { + return new GZipStream(result, CompressionMode.Decompress); + } + // Deflate encoding so returns deflate stream + else if (response.Content.Headers.ContentEncoding.Contains("deflate")) + { + return new DeflateStream(result, CompressionMode.Decompress); + } + + return result; + } + + + + /// + /// Download data from remote via uri query. + /// + /// Uri query + /// An object passed in from the PackageManagement platform that contains APIs that can be used to interact with it + /// + internal static Stream DownloadDataToStream(string query, NuGetRequest request) + { + request.Debug(Messages.DownloadingPackage, query); + + var client = request.Client; + + var response = PathUtility.GetHttpResponse(client, query, (()=>request.IsCanceled), + ((msg, num) => request.Verbose(Resources.Messages.RetryingDownload, msg, num)), (msg)=>request.Verbose(msg), (msg)=>request.Debug(msg)); + + // Check that response was successful or throw exception + if (response == null || !response.IsSuccessStatusCode) + { + request.Warning(Resources.Messages.CouldNotGetResponseFromQuery, query); + return null; + } + + // Read response and write out a stream + var stream = GetStreamBasedOnEncoding(response); + + request.Debug(Messages.CompletedDownload, query); + + return stream; + } + + /// + /// Send an initial request to download data from the server. + /// From the initial request, we may change the host of subsequent calls (if a redirection happens in this initial request) + /// Also, if the initial request sends us less data than the amount we request, then we do not + /// need to issue more requests + /// + /// + /// + /// + /// + /// + internal static Stream InitialDownloadDataToStream(UriBuilder query, int startPoint, int bufferSize, NuGetRequest request) + { + var uri = String.Format(CultureInfo.CurrentCulture, query.Uri.ToString(), startPoint, bufferSize); + request.Debug(Messages.DownloadingPackage, uri); + + var client = request.Client; + + var response = PathUtility.GetHttpResponse(client, uri, (() => request.IsCanceled), + ((msg, num) => request.Verbose(Resources.Messages.RetryingDownload, msg, num)), (msg) => request.Verbose(msg), (msg) => request.Debug(msg)); + + + // Check that response was successful or write error + if (response == null || !response.IsSuccessStatusCode) + { + request.Warning(Resources.Messages.CouldNotGetResponseFromQuery, uri); + return null; + } + + // Read response and write out a stream + var stream = GetStreamBasedOnEncoding(response); + + request.Debug(Messages.CompletedDownload, uri); + + // If the host from the response is different, change the host of the original query + if (!String.Equals(response.RequestMessage.RequestUri.Host, query.Host, StringComparison.OrdinalIgnoreCase)) + { + query.Host = response.RequestMessage.RequestUri.Host; + } + + return stream; + } + + /// + /// Download data from remote via uri query. + /// + /// A file to store the downloaded data. + /// Uri query + /// An object passed in from the PackageManagement platform that contains APIs that can be used to interact with it + /// Credential to pass along to get httpclient + /// Utility class to help track progress + /// + internal static async Task DownloadDataToFileAsync(string fileName, string query, NuGetRequest request, + NetworkCredential networkCredential, ProgressTracker progressTracker) + { + request.Verbose(Messages.DownloadingPackage, query); + + var httpClient = request.Client; + + // try downloading for 3 times + int remainingTry = 3; + long totalDownloaded = 0; + long totalBytesToReceive = 0; + bool cleanUp = false; + CancellationTokenSource cts = new CancellationTokenSource(); ; + Stream input = null; + Timer timer = null; + FileStream output = null; + object lockObject = new object(); + + // function to perform cleanup + Action cleanUpAction = () => { + lock (lockObject) + { + // if clean up is done before, don't need to do again + if (!cleanUp) + { + try + { + // dispose timer + if (timer != null) + { + timer.Change(Timeout.Infinite, Timeout.Infinite); + timer.Dispose(); + } + + // dispose cts token + if (cts != null) + { + cts.Cancel(); + cts.Dispose(); + } + } + catch { } + + try + { + // dispose input and output stream + if (input != null) + { + input.Dispose(); + } + + // it is important that we dispose of the output here, otherwise we may not be able to delete the file + if (output != null) + { + output.Dispose(); + } + + // if the download didn't complete, log verbose message + if (totalBytesToReceive != totalDownloaded) + { + request.Verbose(string.Format(Resources.Messages.IncompleteDownload, totalDownloaded, totalBytesToReceive)); + } + + // if we couldn't download anything + if (totalDownloaded == 0 && File.Exists(fileName)) + { + File.Delete(fileName); + } + } + catch { } + + cleanUp = true; + } + } + }; + + while (remainingTry > 0) + { + // if user cancel the request, no need to do anything + if (request.IsCanceled) + { + break; + } + + input = null; + output = null; + totalDownloaded = 0; + + try + { + // decrease try by 1 + remainingTry -= 1; + + // create new timer and cancellation token source + lock (lockObject) + { + // check every second to see whether request is cancelled + timer = new Timer(_ => + { + if (request.IsCanceled) + { + cleanUpAction(); + } + }, null, 500, 1000); + + cts = new CancellationTokenSource(); + + cleanUp = false; + } + + var response = await httpClient.GetAsync(query, HttpCompletionOption.ResponseHeadersRead, cts.Token); + + if (response.Content != null && response.Content.Headers != null) + { + totalBytesToReceive = response.Content.Headers.ContentLength ?? 0; + // the total amount of bytes we need to download in megabytes + double totalBytesToReceiveMB = (totalBytesToReceive / 1024f) / 1024f; + + // Read response asynchronously and write out a file + // The return value is for the caller to wait for the async operation to complete. + input = await response.Content.ReadAsStreamAsync(); + + // buffer size of 64 KB, this seems to be preferable buffer size, not too small and not too big + byte[] bytes = new byte[1024 * 64]; + output = File.Open(fileName, FileMode.OpenOrCreate); + + int current = 0; + double lastPercent = 0; + + // here we read content that we got from the http response stream into the bytes array + current = await input.ReadAsync(bytes, 0, bytes.Length, cts.Token); + + // report initial progress + request.Progress(progressTracker.ProgressID, progressTracker.StartPercent, + string.Format(CultureInfo.CurrentCulture, Resources.Messages.DownloadingProgress, 0, (totalBytesToReceive / 1024f) / 1024f)); + + while (current > 0) + { + totalDownloaded += current; + + // here we write the bytes array content into the file + await output.WriteAsync(bytes, 0, current, cts.Token); + + double percent = totalDownloaded * 1.0 / totalBytesToReceive; + + // don't want to report too often (slow down performance) + if (percent > lastPercent + 0.1) + { + lastPercent = percent; + // percent between startProgress and endProgress + var progressPercent = progressTracker.ConvertPercentToProgress(percent); + + // report the progress + request.Progress(progressTracker.ProgressID, (int)progressPercent, + string.Format(CultureInfo.CurrentCulture, Resources.Messages.DownloadingProgress, (totalDownloaded / 1024f) / 1024f, totalBytesToReceiveMB)); + } + + // here we read content that we got from the http response stream into the bytes array + current = await input.ReadAsync(bytes, 0, bytes.Length, cts.Token); + } + + // check that we download everything + if (totalDownloaded == totalBytesToReceive) + { + // report that we finished with the download + request.Progress(progressTracker.ProgressID, progressTracker.EndPercent, + string.Format(CultureInfo.CurrentCulture, Resources.Messages.DownloadingProgress, totalDownloaded, totalBytesToReceive)); + + request.Verbose(Messages.CompletedDownload, query); + + break; + } + + // otherwise, we have to retry again + } + + // if request is canceled, don't retry + if (request.IsCanceled) + { + break; + } + + request.Verbose(Resources.Messages.RetryingDownload, query, remainingTry); + } + catch (Exception ex) + { + request.Verbose(ex.Message); + request.Debug(ex.StackTrace); + // if there is exception, we will retry too + request.Verbose(Resources.Messages.RetryingDownload, query, remainingTry); + } + finally + { + cleanUpAction(); + } + } + + return totalDownloaded; + } + } +} diff --git a/src/Microsoft.PackageManagement.NuGetProvider/NugetLightConstant.cs b/src/Microsoft.PackageManagement.NuGetProvider/NugetLightConstant.cs new file mode 100644 index 000000000..b6dd7a031 --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/NugetLightConstant.cs @@ -0,0 +1,70 @@ + +namespace Microsoft.PackageManagement.NuGetProvider +{ + using System; + + public static class NuGetConstant + { + /// + /// Represents the ".nupkg" extension. + /// + public static readonly string PackageExtension = ".nupkg"; + + /// + /// Represents the ".nuspec" extension. + /// + public static readonly string ManifestExtension = ".nuspec"; + + /// + /// Represents the default nuget uri + /// + public static readonly string NugetSite = "www.nuget.org"; + + /// + /// Name of this provider + /// + public static readonly string ProviderName = "NuGet"; + + /// + /// Version of this provider + /// + public static readonly string ProviderVersion = "2.8.5.206"; + + /// + /// Represents a method that the V2 web service supports to find a package + /// + public static readonly string FindPackagesById= "/FindPackagesById()?id='{0}'"; + + /// + /// Represents how many packages to be returned by an api call + /// + public static readonly string SkipAndTop = "&$skip={0}&$top={1}"; + + /// + /// Represents a method that the V2 web service supports to search packages + /// + public static readonly string SearchTerm = "&searchTerm='{0}'&targetFramework=''&includePrerelease={1}"; + + /// + /// Represents a method that the V2 web service supports to search packages with filtering + /// + public static readonly string SearchFilterAllersions = "Search()?$orderby=DownloadCount%20desc,Id"; + + /// + /// Represents a method that the V2 web service supports to search packages with filtering. The result returned is already + /// sorted by download count. + /// + public static readonly string SearchFilter = "Search()?$filter=IsLatestVersion"; + + + /// + /// Config file storing the info as a result of registering a package source + /// + public static readonly string SettingsFileName = "nuget.config"; + + /// + /// The magic unpublished date is 1900-01-01T00:00:00. This is from NugetCore + /// + public static readonly DateTimeOffset Unpublished = new DateTimeOffset(1900, 1, 1, 0, 0, 0, TimeSpan.FromHours(-8)); + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.NuGetProvider/NugetLightRequest.cs b/src/Microsoft.PackageManagement.NuGetProvider/NugetLightRequest.cs new file mode 100644 index 000000000..4cc643090 --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/NugetLightRequest.cs @@ -0,0 +1,1721 @@ +namespace Microsoft.PackageManagement.NuGetProvider +{ + using System; + using System.Text; + using System.Net.Http; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Management.Automation; + using System.Text.RegularExpressions; + using System.Xml; + using System.Xml.Linq; + using System.Collections.Concurrent; + using System.Diagnostics.CodeAnalysis; + using Resources; + using System.Security.Cryptography; + using System.Security; + using System.Runtime.InteropServices; + using System.Net; + using Microsoft.PackageManagement.Provider.Utility; + + /// + /// This class drives the Request class that is an interface exposed from the PackageManagement Platform to the provider to use. + /// + public abstract class NuGetRequest : Request { + private static readonly Regex _regexFastPath = new Regex(@"\$(?[\w,\+,\/,=]*)\\(?[\w,\+,\/,=]*)\\(?[\w,\+,\/,=]*)\\(?[\w,\+,\/,=]*)"); + private static readonly byte[] _nugetBytes = Encoding.UTF8.GetBytes("NuGet"); + private string _configurationFileLocation; + private XDocument _config; + + internal readonly Lazy AllowPrereleaseVersions; + internal readonly Lazy AllVersions; + internal readonly Lazy Contains; + internal readonly Lazy ExcludeVersion; + internal readonly Lazy FilterOnTag; + internal readonly Lazy Headers; + internal readonly Lazy Scope; + internal readonly Lazy InstalledPackages; + + private static IDictionary _registeredPackageSources; + private static IDictionary _checkedUnregisteredPackageSources = new ConcurrentDictionary(); + private string _destinationPath = null; + + internal Lazy SkipValidate; //??? Seems to be a design choice. Why let a user to decide? + // we cannot enable skipdepedencies because this will break downlevel psget which sets skipdependencies to true + //internal Lazy SkipDependencies; + //internal ImplictLazy ContinueOnFailure; + //internal ImplictLazy FindByCanonicalId; + + private HttpClient _httpClient; + private HttpClient _httpClientWithoutAcceptHeader; + + internal const string DefaultConfig = @" + + + +"; + + /// + /// Ctor required by the PackageManagement Platform + /// + protected NuGetRequest() { + FilterOnTag = new Lazy(() => (GetOptionValues("FilterOnTag") ?? new string[0]).ToArray()); + Contains = new Lazy(() => GetOptionValue("Contains")); + ExcludeVersion = new Lazy(() => GetOptionValue("ExcludeVersion").IsTrue()); + AllowPrereleaseVersions = new Lazy(() => GetOptionValue("AllowPrereleaseVersions").IsTrue()); + AllVersions = new Lazy(() => GetOptionValue("AllVersions").IsTrue()); + + SkipValidate = new Lazy(() => GetOptionValue("SkipValidate").IsTrue()); + Scope = new Lazy(() => GetOptionValue("Scope")); + + //SkipDependencies = new Lazy(() => GetOptionValue("SkipDependencies").IsTrue()); + //ContinueOnFailure = new ImplictLazy(() => GetOptionValue("ContinueOnFailure").IsTrue()); + //FindByCanonicalId = new ImplictLazy(() => GetOptionValue("FindByCanonicalId").IsTrue()); + + Headers = new Lazy(() => (GetOptionValues("Headers") ?? new string[0]).ToArray()); + InstalledPackages = new Lazy(() => (GetInstalledPackagesOptionValue()).ToArray()); + } + + // parse the list of installed packages + private IEnumerable GetInstalledPackagesOptionValue() + { + // get possible installed packages + var installedPackages = GetOptionValues("InstalledPackages") ?? new string[0]; + + // parse the installed package options passed in + foreach (var installedPackage in installedPackages) + { + // we assume that the name passed in is something like jquery#1.2.5 + string[] nameAndVersion = installedPackage.Split(new[] { "!#!" }, StringSplitOptions.None); + + var package = new PackageBase(); + + // only name passed in, no version + if (nameAndVersion.Count() == 1) + { + package.Id = nameAndVersion[0]; + yield return package; + } + else if (nameAndVersion.Count() == 2) + { + // this means there is version + SemanticVersion semVers = null; + + try + { + // convert version to semvers + semVers = new SemanticVersion(nameAndVersion[1]); + } + catch + { + // not a valid version, ignores this entry + continue; + } + + // set name and version of this installed packages + package.Id = nameAndVersion[0]; + package.Version = semVers.Version.ToStringSafe(); + + yield return package; + } + } + } + + /// + /// Package sources + /// + internal string[] OriginalSources {get; set;} + + /// + /// Package installation location used by get-installedpackages. + /// + internal IEnumerable InstalledPath + { + get + { + var path = GetOptionValue("Destination"); + + if (!string.IsNullOrWhiteSpace(path)) + { + yield return Path.GetFullPath(path); + } + else + { + // If a user does not specify -destination, we will look into the default locations. + yield return AllUserDefaultInstallLocation; + yield return CurrentUserDefaultInstallLocation; + } + } + } + + /// + /// Package destination path + /// + internal string Destination { + get { + if (_destinationPath != null) + { + return _destinationPath; + } + var path = GetOptionValue("Destination"); + + if (!string.IsNullOrWhiteSpace(path)) + { + _destinationPath = Path.GetFullPath(path); + return _destinationPath; + } + + // If a user does not give -destination for the install, we put the package under $env:USERPROFILE\nuget\packages folder + // or $env:programfiles\NuGet\Packages\ if you are an admin. + try + { + var scope = (Scope == null) ? null : Scope.Value; + scope = string.IsNullOrWhiteSpace(scope) ? Constants.AllUsers : scope; + string basePath; + + if (scope.EqualsIgnoreCase(Constants.CurrentUser)) + { + // Does not matter whether elevated or not + basePath = CurrentUserDefaultInstallLocation; + } + else if (ProviderServices.IsElevated) + { + //Scope=AllUser or No Scope but elevated + basePath = AllUserDefaultInstallLocation; + } + else + { + //Scope=AllUser but not elevated + WriteError(ErrorCategory.InvalidOperation, ErrorCategory.InvalidOperation.ToString(), + Constants.Messages.InstallRequiresCurrentUserScopeParameterForNonAdminUser, AllUserDefaultInstallLocation, CurrentUserDefaultInstallLocation); + return string.Empty; + } + + if (!Directory.Exists(basePath)) + { + Directory.CreateDirectory(basePath); + } + _destinationPath = basePath; + return basePath; + } + catch (Exception e) + { + e.Dump(this); + WriteError(ErrorCategory.InvalidArgument, "Destination", Constants.Messages.MissingRequiredParameter, "Destination"); + return string.Empty; + } + } + } + + + internal string CurrentUserDefaultInstallLocation + { + get + { +#if CORECLR + return Path.Combine(Environment.GetEnvironmentVariable("LocalAppData"), "PackageManagement", "NuGet", "Packages"); +#else + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "PackageManagement", "NuGet", "Packages"); +#endif + } + + } + + internal string AllUserDefaultInstallLocation + { + get + { +#if CORECLR + return Path.Combine(Environment.GetEnvironmentVariable("ProgramFiles"), "NuGet", "Packages"); +#else + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "NuGet", "Packages"); +#endif + } + } + + + /// + /// Get the PackageItem object from the fast path + /// + /// + /// + internal PackageItem GetPackageByFastpath(string fastPath) { + Debug(Resources.Messages.DebugInfoCallMethod3, "NuGetRequest", "GetPackageByFastpath", fastPath); + + string sourceLocation; + string id; + string version; + string[] sources; + + if (TryParseFastPath(fastPath, out sourceLocation, out id, out version, out sources)) { + var source = ResolvePackageSource(sourceLocation); + + if (source.IsSourceAFile) { + return GetPackageByFilePath(sourceLocation); + } + + // repository should not be null if source is not a file + if (source.Repository == null) + { + return null; + } + + // Have to find package again to get possible dependencies + var pkg = source.Repository.FindPackage(id, new SemanticVersion(version), this); + + // only finds the pkg if it is a file. so we don't return it here + // otherwise we make another download request + return new PackageItem { + FastPath = fastPath, + Package = pkg, + PackageSource = source, + Sources = sources + }; + } + + return null; + } + + /// + /// Get a package object from the package manifest file + /// + /// package manifest file path + /// package Id or Name + /// + internal PackageItem GetPackageByFilePath(string filePath, string packageName) + { + Debug(Resources.Messages.DebugInfoCallMethod3, "GetPackageByFilePath", filePath, packageName); + + PackageBase package = null; + try { + + if (NuGetPathUtility.IsManifest(filePath)) { + //.nuspec + package = PackageUtility.ProcessNuspec(filePath); + + } else if (NuGetPathUtility.IsPackageFile(filePath)) { + //.nupkg or .zip + //The file name may contains version. ex: jQuery.2.1.1.nupkg + package = PackageUtility.DecompressFile(filePath, packageName); + + } else { + Warning(Resources.Messages.InvalidFileExtension, filePath); + + } + + } catch (Exception ex) { + ex.Dump(this); + } + + if (package == null) { + return null; + } + + var source = ResolvePackageSource(filePath); + + return new PackageItem { + FastPath = MakeFastPath(source, package.Id, package.Version), + PackageSource = source, + Package = package, + IsPackageFile = true, + FullPath = filePath, + }; + } + + /// + /// Get a package object from the package manifest file + /// + /// package manifest file path + /// + internal PackageItem GetPackageByFilePath(string filePath) { + Debug(Resources.Messages.DebugInfoCallMethod3, "NuGetRequest", "GetPackageByFilePath", filePath); + var packageName = Path.GetFileNameWithoutExtension(filePath); + + var pkgItem = GetPackageByFilePath(filePath, packageName); + + return pkgItem; + } + + /// + /// Unregister the package source + /// + /// package source id or name + internal void RemovePackageSource(string id) { + Debug(Resources.Messages.DebugInfoCallMethod3, "NuGetRequest", "RemovePackageSource", id); + var config = Config; + if (config == null) { + return; + } + + try { + + XElement configuration = config.ElementsNoNamespace("configuration").FirstOrDefault(); + if (configuration == null) + { + return; + } + + XElement packageSources = configuration.ElementsNoNamespace("packageSources").FirstOrDefault(); + if (packageSources == null) + { + return; + } + + var nodes = packageSources.Elements("add"); + if (nodes == null) { + return; + } + + foreach (XElement node in nodes) { + + if (node.Attribute("key") != null && String.Equals(node.Attribute("key").Value, id, StringComparison.OrdinalIgnoreCase)) { + // remove itself + node.Remove(); + Config = config; + Verbose(Resources.Messages.RemovedPackageSource, id); + break; + } + + } + + if (_registeredPackageSources.ContainsKey(id)) + { + _registeredPackageSources.Remove(id); + } + + if (_checkedUnregisteredPackageSources.ContainsKey(id)) + { + _checkedUnregisteredPackageSources.Remove(id); + } + + + //var source = config.SelectNodes("/configuration/packageSources/add").Cast().FirstOrDefault(node => String.Equals(node.Attributes["key"].Value, id, StringComparison.CurrentCultureIgnoreCase)); + + //if (source != null) + //{ + // source.ParentNode.RemoveChild(source); + // Config = config; + // Verbose(Resources.Messages.RemovedPackageSource, id); + //} + } catch (Exception ex) { + ex.Dump(this); + } + } + + /// + /// Register the package source + /// + /// package source name + /// package source location + /// is the source trusted + /// need validate before storing the information to config file + internal void AddPackageSource(string name, string location, bool isTrusted, bool isValidated) { + + Debug(Resources.Messages.DebugInfoCallMethod, "NuGetRequest", string.Format(CultureInfo.InvariantCulture, "AddPackageSource - name= {0}, location={1}", name, location)); + try { + // here the source is already validated by the caller + var config = Config; + if (config == null) { + return; + } + + XElement source = null; + XElement packageSources = null; + // Check whether there is an existing node with the same name + var configuration = config.ElementsNoNamespace("configuration").FirstOrDefault(); + if (configuration != null) + { + packageSources = configuration.ElementsNoNamespace("packageSources").FirstOrDefault(); + if (packageSources != null) + { + source = packageSources.Elements("add").FirstOrDefault(node => + node.Attribute("key") != null && String.Equals(node.Attribute("key").Value, name, StringComparison.OrdinalIgnoreCase)); + } + } + else + { + // create configuration node if it does not exist + configuration = new XElement("configuration"); + // add that to the config + config.Add(configuration); + } + + // There is no existing node with the same name. So we have to create one. + if (source == null) + { + // if packagesources is null we have to create that too + if (packageSources == null) + { + // create packagesources node + packageSources = new XElement("packageSources"); + // add that to the config + configuration.Add(packageSources); + } + + // Create new source + source = new XElement("add"); + // Add that to packagesource + packageSources.Add(source); + } + + // Now set the source node properties + source.SetAttributeValue("key", name); + source.SetAttributeValue("value", location); + if (isValidated) + { + source.SetAttributeValue("validated", true.ToString()); + } + if (isTrusted) + { + source.SetAttributeValue("trusted", true.ToString()); + } + + // Write back to the config file + Config = config; + + // Add or set the source node from the dictionary depends on whether it was there + if (_registeredPackageSources.ContainsKey(name)) + { + var packageSource = _registeredPackageSources[name]; + packageSource.Name = name; + packageSource.Location = location; + packageSource.Trusted = isTrusted; + packageSource.IsRegistered = true; + packageSource.IsValidated = isValidated; + } + else + { + _registeredPackageSources.Add(name, new PackageSource + { + Request = this, + Name = name, + Location = location, + Trusted = isTrusted, + IsRegistered = true, + IsValidated = isValidated, + }); + } + + } catch (Exception ex) { + ex.Dump(this); + } + } + + /// + /// Check if the package source location is valid + /// + /// + /// + internal bool ValidateSourceLocation(string location) { + + Debug(Resources.Messages.DebugInfoCallMethod3, "NuGetRequest", "ValidateSourceLocation", location); + //Handling http: or file: cases + if (Uri.IsWellFormedUriString(location, UriKind.Absolute)) { + return NuGetPathUtility.ValidateSourceUri(SupportedSchemes, new Uri(location), this); + } + try { + //UNC or local file + if (Directory.Exists(location) || File.Exists(location)) { + return true; + } + } catch { + } + return false; + } + + /// + /// Supported package source schemes by this provider + /// + internal static IEnumerable SupportedSchemes { + get { + return NuGetProvider.Features[Constants.Features.SupportedSchemes]; + } + } + + /// + /// Return the package source object + /// + /// The package source name to search for + /// package source object + internal PackageSource FindRegisteredSource(string name) { + + Debug(Resources.Messages.DebugInfoCallMethod3, "NuGetRequest", "FindRegisteredSource", name); + var srcs = RegisteredPackageSources; + + if (srcs == null) { + return null; + } + if (srcs.ContainsKey(name)) { + return srcs[name]; + } + + var src = srcs.Values.FirstOrDefault(each => LocationCloseEnoughMatch(name, each.Location)); + return src; + } + + /// + /// Communicate to the PackageManagement platform about the package info + /// + /// + /// + /// + internal bool YieldPackages(IEnumerable packageReferences, string searchKey) { + var foundPackage = false; + if (packageReferences == null) { + return false; + } + + Debug(Resources.Messages.Iterating, searchKey); + + IEnumerable packageItems = packageReferences; + + int count = 0; + + foreach (var pkg in packageItems) { + foundPackage = true; + try { + if (!YieldPackage(pkg, searchKey)) { + break; + } + + count++; + } catch (Exception e) { + e.Dump(this); + return false; + } + } + + Verbose(Resources.Messages.TotalPackageYield, count, searchKey); + Debug(Resources.Messages.CompletedIterating, searchKey); + + return foundPackage; + } + + private string MakeTagId(PackageItem pkg) + { + if (pkg == null || pkg.Package == null) + { + return string.Empty; + } + + // the tag id will look like this zlib#1.2.8.8#Jean-loup Gailly;Mark Adler + return string.Format(CultureInfo.CurrentCulture, "{0}#{1}", pkg.Package.Id, pkg.Package.Version.ToString()); + } + + /// + /// HttpClient with Accept-CharSet and Accept-Encoding Header + /// We want to reuse HttpClient + /// + internal HttpClient Client + { + get + { + if (_httpClient == null) + { + _httpClient = PathUtility.GetHttpClientHelper(CredentialUsername, CredentialPassword, WebProxy); + + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept-Charset", "UTF-8"); + // Request for gzip and deflate encoding to make the response lighter. + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept-Encoding", "gzip,deflate"); + + foreach (var header in Headers.Value) + { + // header is in the format "A=B" because OneGet doesn't support Dictionary parameters + if (!String.IsNullOrEmpty(header)) + { + var headerSplit = header.Split(new string[] { "=" }, 2, StringSplitOptions.RemoveEmptyEntries); + + // ignore wrong entries + if (headerSplit.Count() == 2) + { + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation(headerSplit[0], headerSplit[1]); + } + else + { + Warning(Messages.HeaderIgnored, header); + } + } + } + } + + return _httpClient; + } + } + + /// + /// HttpClient without any Accept header (this will only have User-Agent header) + /// + internal HttpClient ClientWithoutAcceptHeader + { + get + { + if (_httpClientWithoutAcceptHeader == null) + { + _httpClientWithoutAcceptHeader = PathUtility.GetHttpClientHelper(CredentialUsername, CredentialPassword, WebProxy); + } + + return _httpClientWithoutAcceptHeader; + } + } + + /// + /// Communicate to the PackageManagement platform about the package info + /// + /// + /// + /// + /// + internal bool YieldPackage(PackageItem pkg, string searchKey, string destinationPath=null) + { + try + { + if (YieldSoftwareIdentity(pkg.FastPath, pkg.Package.Id, pkg.Package.Version.ToString(), "semver", pkg.Package.Summary, pkg.PackageSource.Name, searchKey, pkg.FullPath, pkg.PackageFilename) != null) + { + if (pkg.Package.DependencySetList != null) + { + //iterate thru the dependencies and add them to the software identity. + foreach (PackageDependencySet depSet in pkg.Package.DependencySetList) + { + foreach (var dep in depSet.Dependencies) + { + AddDependency(NuGetConstant.ProviderName, dep.Id, dep.DependencyVersion.ToStringSafe(), null, null); + } + } + } + + // downlevel machine does not have AddTagId interface in request object so it will return null + // hence we can't check it here. + AddTagId(MakeTagId(pkg)); + + // if we need to report installation location, add a payload and add directory to that + if (!string.IsNullOrWhiteSpace(destinationPath)) + { + string payload = AddPayload(); + + if (string.IsNullOrWhiteSpace(payload)) + { + return false; + } + + AddDirectory(payload, Path.GetFileName(destinationPath), Path.GetDirectoryName(destinationPath), null, true); + } + + if (AddMetadata(pkg.FastPath, "copyright", pkg.Package.Copyright) == null) { + return false; + } + if (AddMetadata(pkg.FastPath, "description", pkg.Package.Description) == null) { + return false; + } + if (AddMetadata(pkg.FastPath, "licenseNames", pkg.Package.LicenseNames) == null) + { + return false; + } + if (AddMetadata(pkg.FastPath, "requireLicenseAcceptance", pkg.Package.RequireLicenseAcceptance.ToString()) == null) + { + return false; + } + if (AddMetadata(pkg.FastPath, "language", pkg.Package.Language) == null) { + return false; + } + if (AddMetadata(pkg.FastPath, "releaseNotes", pkg.Package.ReleaseNotes) == null) { + return false; + } + if (AddMetadata(pkg.FastPath, "isLatestVersion", pkg.Package.IsLatestVersion.ToString()) == null) + { + return false; + } + if (AddMetadata(pkg.FastPath, "isAbsoluteLatestVersion", pkg.Package.IsAbsoluteLatestVersion.ToString()) == null) + { + return false; + } + if (pkg.Package.MinClientVersion != null && AddMetadata(pkg.FastPath, "minClientVersion", pkg.Package.MinClientVersion.ToString()) == null) + { + return false; + } + + if (pkg.Package.VersionDownloadCount != -1) + { + if (AddMetadata(pkg.FastPath, "versionDownloadCount", pkg.Package.VersionDownloadCount.ToString(CultureInfo.CurrentCulture)) == null) + { + return false; + } + } + + if (pkg.Package.DownloadCount != -1) + { + if (AddMetadata(pkg.FastPath, "downloadCount", pkg.Package.DownloadCount.ToString(CultureInfo.CurrentCulture)) == null) + { + return false; + } + } + + if (pkg.Package.PackageSize != -1) + { + if (AddMetadata(pkg.FastPath, "packageSize", pkg.Package.PackageSize.ToString(CultureInfo.CurrentCulture)) == null) + { + return false; + } + } + if (pkg.Package.Published != null) { + if (AddMetadata(pkg.FastPath, "published", pkg.Package.Published.ToString()) == null) { + return false; + } + } + if (pkg.Package.Created != null) + { + if (AddMetadata(pkg.FastPath, "created", pkg.Package.Created.ToString()) == null) + { + return false; + } + } + if (pkg.Package.LastEdited != null) + { + if (AddMetadata(pkg.FastPath, "lastEdited", pkg.Package.LastEdited.ToString()) == null) + { + return false; + } + } + if (pkg.Package.LastUpdated != null) + { + if (AddMetadata(pkg.FastPath, "lastUpdated", pkg.Package.LastUpdated.ToString()) == null) + { + return false; + } + } + if (AddMetadata(pkg.FastPath, "tags", pkg.Package.Tags) == null) { + return false; + } + if (AddMetadata(pkg.FastPath, "title", pkg.Package.Title) == null) { + return false; + } + if (AddMetadata(pkg.FastPath, "developmentDependency", pkg.Package.DevelopmentDependency.ToString()) == null) { + return false; + } + if (AddMetadata(pkg.FastPath, "FromTrustedSource", pkg.PackageSource.Trusted.ToString()) == null) { + return false; + } + if (pkg.Package.LicenseUrl != null && !String.IsNullOrWhiteSpace(pkg.Package.LicenseUrl.ToString())) + { + if (AddLink(pkg.Package.LicenseUrl, "license", null, null, null, null, null) == null) { + return false; + } + } + if (pkg.Package.ProjectUrl != null && !String.IsNullOrWhiteSpace(pkg.Package.ProjectUrl.ToString())) + { + if (AddLink(pkg.Package.ProjectUrl, "project", null, null, null, null, null) == null) { + return false; + } + } + if (pkg.Package.ReportAbuseUrl != null && !String.IsNullOrWhiteSpace(pkg.Package.ReportAbuseUrl.ToString())) + { + if (AddLink(pkg.Package.ReportAbuseUrl, "abuse", null, null, null, null, null) == null) { + return false; + } + } + if (pkg.Package.IconUrl != null && !String.IsNullOrWhiteSpace(pkg.Package.IconUrl.ToString())) + { + if (AddLink(pkg.Package.IconUrl, "icon", null, null, null, null, null) == null) { + return false; + } + } + if (pkg.Package.GalleryDetailsUrl != null && !String.IsNullOrWhiteSpace(pkg.Package.GalleryDetailsUrl.ToString())) + { + if (AddLink(pkg.Package.GalleryDetailsUrl, "galleryDetails", null, null, null, null, null) == null) + { + return false; + } + } + if (pkg.Package.LicenseReportUrl != null && !String.IsNullOrWhiteSpace(pkg.Package.LicenseReportUrl.ToString())) + { + if (AddLink(pkg.Package.LicenseReportUrl, "licenseReport", null, null, null, null, null) == null) + { + return false; + } + } + if (pkg.Package.Authors.Any(author => AddEntity(author.Trim(), author.Trim(), "author", null) == null)) { + return false; + } + + if (pkg.Package.Owners.Any(owner => AddEntity(owner.Trim(), owner.Trim(), "owner", null) == null)) { + return false; + } + + var pkgBase = pkg.Package as PackageBase; + if (pkgBase != null) + { + if (pkgBase.AdditionalProperties != null) + { + foreach (var property in pkgBase.AdditionalProperties) + { + if (AddMetadata(pkg.FastPath, property.Key, property.Value) == null) + { + return false; + } + } + } + } + } else { + return false; + } + } catch (Exception e) { + e.Dump(this); + return false; + } + return true; + } + + internal bool MinAndMaxVersionMatched(SemanticVersion packageVersion, string minimumVersion, string maximumVersion, bool minInclusive, bool maxInclusive) + { + if (!string.IsNullOrWhiteSpace(minimumVersion)) + { + // Check whether we are checking for min inclusive version + if (minInclusive) + { + // version too small, not what we are looking for + if (packageVersion < new SemanticVersion(minimumVersion)) + { + return false; + } + } + else + { + if (packageVersion <= new SemanticVersion(minimumVersion)) + { + return false; + } + } + } + + if (!string.IsNullOrWhiteSpace(maximumVersion)) + { + // Check whether we are checking for max inclusive version + if (maxInclusive) + { + // version too big, not what we are looking for + if (packageVersion > new SemanticVersion(maximumVersion)) + { + return false; + } + } + else + { + if (packageVersion >= new SemanticVersion(maximumVersion)) + { + return false; + } + } + } + + return true; + } + + /// + /// Search in the destination of the request to checks whether the package name is installed. + /// Returns true if at least 1 package is installed + /// + /// + /// + /// + /// + /// + /// + /// + /// + internal bool GetInstalledPackages(string name, string requiredVersion, string minimumVersion, string maximumVersion, bool minInclusive = true, bool maxInclusive = true, bool terminateFirstFound = false) + { + try + { + bool found = false; + var paths = InstalledPath.Where(Directory.Exists).ToArray(); + + // if directory does not exist then just return false + if (!paths.Any()) + { + return false; + } + + // look in the destination directory for directories that contain *.nupkg & .nuspec files. + var subdirs = paths.SelectMany(Directory.EnumerateDirectories); + + foreach (var subdir in subdirs) + { + //reset the flag when we begin a folder + var isDup = false; + + var nupkgs = Directory.EnumerateFiles(subdir, "*.nupkg", SearchOption.TopDirectoryOnly).Union( + Directory.EnumerateFiles(subdir, "*.nuspec", SearchOption.TopDirectoryOnly)); + + foreach (var pkgFile in nupkgs) + { + + if (isDup) + { + continue; + } + + //As the package name has to be in the installed file path, check if it is true + if (!String.IsNullOrWhiteSpace(name) && !NuGetProvider.IsNameMatch(name, pkgFile)) + { + //not the right package + continue; + } + + //unpack the package + var existFileName = Path.GetFileNameWithoutExtension(pkgFile); + + var pkgItem = GetPackageByFilePath(pkgFile, existFileName); + + if (pkgItem != null && pkgItem.IsInstalled) + { + //A user does not provide any package name in the commandeline, return them all + if (string.IsNullOrWhiteSpace(name)) + { + isDup = true; + if (!YieldPackage(pkgItem, existFileName)) + { + return found; + } + } + else + { + + //check if the version matches + if (!string.IsNullOrWhiteSpace(requiredVersion)) + { + if (pkgItem.Package.Version != new SemanticVersion(requiredVersion)) + { + continue; + } + } + else + { + // if min and max version not matched + if (!MinAndMaxVersionMatched(pkgItem.Package.Version, minimumVersion, maximumVersion, minInclusive, maxInclusive)) + { + continue; + } + + if (terminateFirstFound) + { + // if we are searching for a name and terminatefirstfound is set to true, then returns here + return true; + } + } + + //found the match + isDup = true; + YieldPackage(pkgItem, existFileName); + found = true; + + } //end of else + + } //end of if (pkgItem...) + } //end of foreach + }//end of foreach subfolder + + return found; + } + catch (Exception ex) + { + ex.Dump(this); + return false; + } + } + + /// + /// Get the package based on given package id + /// + /// Package id or name + /// + /// + /// + /// + /// + /// + /// + internal IEnumerable GetPackageById(string name, NuGetRequest request, string requiredVersion = null, + string minimumVersion = null, string maximumVersion = null, bool minInclusive = true, bool maxInclusive = true) { + if (String.IsNullOrWhiteSpace(name)) + { + return Enumerable.Empty(); + } + + return SelectedSources.AsParallel().WithMergeOptions(ParallelMergeOptions.NotBuffered).SelectMany(source => GetPackageById(source, name, request, requiredVersion, minimumVersion, maximumVersion, minInclusive, maxInclusive)); + } + + /// + /// Encrypting a path containing package source location, id, version and source + /// + /// package source + /// package id + /// package version + /// + internal string MakeFastPath(PackageSource source, string id, string version) { + return String.Format(CultureInfo.InvariantCulture, @"${0}\{1}\{2}\{3}", source.Serialized, id.ToBase64(), version.ToBase64(), (Sources ?? new string[0]).Select(each => each.ToBase64()).SafeAggregate((current, each) => current + "|" + each)); + } + + /// + /// Return all registered package sources + /// + internal IEnumerable SelectedSources { + get { + if (IsCanceled) { + yield break; + } + + //get sources from user's input + var sources = (OriginalSources ?? Enumerable.Empty()).Union(PackageSources ?? Enumerable.Empty()).ToArray(); + //get sources from config file that registered earlier + var pkgSources = RegisteredPackageSources; + + Debug(Resources.Messages.RegisteredSources, pkgSources.Count, NuGetConstant.ProviderName); + + //If a user does not provider -source, we use the registered ones + if (sources.Length == 0) { + // return them all. + foreach (var src in pkgSources.Values) { + Debug(src.Name); + // set the request of the registered one to the current request because it may have additional information like credential + src.Request = this; + yield return src; + } + yield break; + } + + // otherwise, return package sources that match the items given. + foreach (var src in sources) + { + // Check whether we've already processed this item before + if (_checkedUnregisteredPackageSources.ContainsKey(src)) + { + _checkedUnregisteredPackageSources[src].Request = this; + yield return _checkedUnregisteredPackageSources[src]; + continue; + } + + // check to see if we have a source with either that name + // or that URI first. + if (pkgSources.ContainsKey(src)) + { + Debug(Resources.Messages.FoundRegisteredSource, src, NuGetConstant.ProviderName); + _checkedUnregisteredPackageSources.Add(src, pkgSources[src]); + pkgSources[src].Request = this; + yield return pkgSources[src]; + continue; + } + + var srcLoc = src; + var found = false; + foreach (var byLoc in pkgSources.Values) + { + // srcLoc does not match byLoc.Location, try to check for srcLoc with "/" appended at the end + if (!string.Equals(byLoc.Location, srcLoc, StringComparison.OrdinalIgnoreCase) + && !string.Equals(byLoc.Location, string.Concat(srcLoc, "/"), StringComparison.OrdinalIgnoreCase) + && !string.Equals(string.Concat(byLoc.Location, "/"), srcLoc, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + // in this case, do not store the srcLoc into the dictionary, otherwise, the provider may resolve http://www.nuget.org/api/v2 to some source with location + // http://www.nuget.org/api/v2/ and oneget will raise an error (it thinks we are dishonest :() + byLoc.Location = srcLoc; + // set request of byloc to this because this request may have credential information + byLoc.Request = this; + yield return byLoc; + found = true; + } + if (found) + { + continue; + } + + Debug(Resources.Messages.NotFoundRegisteredSource, src, NuGetConstant.ProviderName); + + // doesn't look like we have this as a source. + if (Uri.IsWellFormedUriString(src, UriKind.Absolute)) + { + // we have been passed in an URI + var srcUri = new Uri(src); + if (SupportedSchemes.Contains(srcUri.Scheme.ToLower())) + { + // it's one of our supported uri types. + var isValidated = false; + + if (!SkipValidate.Value) + { + try{ + isValidated = NuGetPathUtility.ValidateSourceUri(SupportedSchemes, srcUri, this); + } + catch (Exception ex) + { + ex.Dump(this); + } + } + + if (SkipValidate.Value || isValidated) + { + Debug(Resources.Messages.SuccessfullyValidated, src); + + yield return new PackageSource + { + Request = this, + Location = srcUri.ToString(), + Name = srcUri.ToString(), + Trusted = false, + IsRegistered = false, + IsValidated = isValidated, + }; + continue; + } + Warning(Constants.Messages.UnableToResolveSource, src); + continue; + } + + // Not a valid location? + Warning(Constants.Messages.UnableToResolveSource, src); + continue; + } + + // is it a file path? + if (Directory.Exists(src)) + { + Debug(Resources.Messages.SourceIsADirectory, src); + + PackageSource newSource = new PackageSource + { + Request = this, + Location = src, + Name = src, + Trusted = true, + IsRegistered = false, + IsValidated = true, + }; + _checkedUnregisteredPackageSources.Add(src, newSource); + yield return newSource; + } + else + { + // Not a valid location? + Warning(Constants.Messages.UnableToResolveSource, src); + } + } + } + } + + private XDocument Config { + get { + if (_config == null) + { + Debug(Resources.Messages.LoadingConfigurationFile, ConfigurationFileLocation); + + XDocument doc = null; + + try{ + using (FileStream fs = new FileStream(ConfigurationFileLocation, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { + // load the xdocument from the file stream + doc = XmlUtility.LoadSafe(fs, ignoreWhiteSpace: true); + } + + Debug(Resources.Messages.LoadedConfigurationFile, ConfigurationFileLocation); + + if (doc.Root != null && doc.Root.Name != null && String.Equals(doc.Root.Name.LocalName, "configuration", StringComparison.OrdinalIgnoreCase)) { + _config = doc; + return _config; + } + Warning(Resources.Messages.MissingConfigurationElement, ConfigurationFileLocation); + } + catch (Exception e) + { + // a bad xml doc or a folder gets deleted somehow + Warning(e.Message); + //string dir = Path.GetDirectoryName(ConfigurationFileLocation); + //if (dir != null && !Directory.Exists(dir)) + //{ + // Debug(Resources.Messages.CreateDirectory, dir); + // Directory.CreateDirectory(dir); + //} + + //Debug(Resources.Messages.UseDefaultConfig); + //_config.Load(new MemoryStream((byte[])Encoding.UTF8.GetBytes(DefaultConfig))); + } + } + + return _config; + } + set { + + if (value == null) { + Debug(Resources.Messages.SettingConfigurationToNull); + return; + } + + _config = value; + + Verbose(Resources.Messages.SavingConfigurationWithFile, ConfigurationFileLocation); + var stringBuilder = new System.Text.StringBuilder(); + + var settings = new XmlWriterSettings + { + OmitXmlDeclaration = false, + Indent = true, + NewLineOnAttributes = true, + NamespaceHandling = NamespaceHandling.OmitDuplicates + }; + + using (var xmlWriter = XmlWriter.Create(stringBuilder, settings)) + { + _config.Save(xmlWriter); + File.WriteAllText(ConfigurationFileLocation, _config.ToString()); + } + } + } + + private string ConfigurationFileLocation { + get { + if (String.IsNullOrWhiteSpace(_configurationFileLocation)) + { + // get the value from the request + var path = GetOptionValue("ConfigFile"); + + if (!String.IsNullOrWhiteSpace(path)) + { + _configurationFileLocation = path; + + if (!File.Exists(_configurationFileLocation)) + { + WriteError(ErrorCategory.InvalidArgument, _configurationFileLocation, Resources.Messages.FileNotFound); + } + + } else { + var appdataFolder = Environment.GetEnvironmentVariable("appdata"); + _configurationFileLocation = Path.Combine(appdataFolder, "NuGet", NuGetConstant.SettingsFileName); + + //create directory if does not exist + string dir = Path.GetDirectoryName(_configurationFileLocation); + if (dir != null && !Directory.Exists(dir)) + { + Debug(Resources.Messages.CreateDirectory, dir); + Directory.CreateDirectory(dir); + } + //create place holder config file + if (!File.Exists(_configurationFileLocation)) + { + Debug(Resources.Messages.CreateFile, _configurationFileLocation); + File.WriteAllText(_configurationFileLocation, DefaultConfig); + } + } + } + + return _configurationFileLocation; + } + } + + private IDictionary RegisteredPackageSources + { + get { + if (_registeredPackageSources == null) + { + _registeredPackageSources = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + try + { + Debug(Resources.Messages.DebugInfoCallMethod3, "NuGetRequest", "RegisteredPackageSources", ConfigurationFileLocation); + + var config = Config; + if (config != null) + { + // get the configuration node + var configuration = config.ElementsNoNamespace("configuration").FirstOrDefault(); + if (configuration != null) + { + // get the packageSources node + var packageSources = configuration.ElementsNoNamespace("packageSources").FirstOrDefault(); + + if (packageSources != null) + { + _registeredPackageSources = packageSources.Elements("add") + .Where(each => each.Attribute("key") != null && each.Attribute("value") != null) + .ToDictionaryNicely(each => each.Attribute("key").Value, each => + new PackageSource + { + Request = this, + Name = each.Attribute("key").Value, + Location = each.Attribute("value").Value, + Trusted = each.Attribute("trusted") != null && each.Attribute("trusted").Value.IsTrue(), + IsRegistered = true, + IsValidated = each.Attribute("validated") != null && each.Attribute("validated").Value.IsTrue(), + }, StringComparer.OrdinalIgnoreCase); + + } + } + } + } + catch (Exception) + { + _registeredPackageSources = new ConcurrentDictionary(); + } + } + + return _registeredPackageSources; + } + } + + private IEnumerable GetPackageById(PackageSource source, + string name, + NuGetRequest request, + string requiredVersion = null, + string minimumVersion = null, + string maximumVersion = null, + bool minInclusive = true, + bool maxInclusive = true) { + try { + Debug(Resources.Messages.DebugInfoCallMethod3, "NuGetRequest", "GetPackageById", name); + + // source should be attached to a repository + if (source.Repository == null) + { + return Enumerable.Empty(); + } + + // otherwise fall back to traditional behavior + var pkgs = source.Repository.FindPackagesById(name, request); + + // if required version not specified then don't use unlisted version + // unlisted version is the one that has published year as 1900 + if (string.IsNullOrWhiteSpace(requiredVersion)) + { + pkgs = pkgs.Where(pkg => pkg.Published.HasValue && pkg.Published.Value.Year > 1900); + } + + if (AllVersions.Value){ + //Display versions from lastest to oldest + pkgs = (from p in pkgs select p).OrderByDescending(x => x.Version); + } + + + //A user does not provide version info, we choose the latest + if (!AllVersions.Value && (String.IsNullOrWhiteSpace(requiredVersion) && String.IsNullOrWhiteSpace(minimumVersion) && String.IsNullOrWhiteSpace(maximumVersion))) + { + + if (AllowPrereleaseVersions.Value || source.Repository.IsFile) { + //Handling something like Json.NET 7.0.1-beta3 as well as local repository + pkgs = from p in pkgs + group p by p.Id + into newGroup + select newGroup.Aggregate((current, next) => (next.Version > current.Version) ? next : current); + + } else { + pkgs = from p in pkgs where p.IsLatestVersion select p; + } + } + + pkgs = FilterOnContains(pkgs); + pkgs = FilterOnTags(pkgs); + + var results = FilterOnVersion(pkgs, requiredVersion, minimumVersion, maximumVersion, minInclusive, maxInclusive) + .Select(pkg => new PackageItem { + Package = pkg, + PackageSource = source, + FastPath = MakeFastPath(source, pkg.Id, pkg.Version.ToString()) + }); + return results; + } catch (Exception e) { + e.Dump(this); + return Enumerable.Empty(); + } + } + + private PackageSource ResolvePackageSource(string nameOrLocation) { + Debug(Resources.Messages.DebugInfoCallMethod3, "NuGetRequest", "ResolvePackageSource", nameOrLocation); + + if (IsCanceled) { + return null; + } + + var source = FindRegisteredSource(nameOrLocation); + if (source != null) { + Debug(Resources.Messages.FoundRegisteredSource, nameOrLocation, NuGetConstant.ProviderName); + return source; + } + + Debug(Resources.Messages.NotFoundRegisteredSource, nameOrLocation, NuGetConstant.ProviderName); + + try { + // is the given value a filename? + if (File.Exists(nameOrLocation)) { + Debug(Resources.Messages.SourceIsAFilePath, nameOrLocation); + + return new PackageSource() { + Request = this, + IsRegistered = false, + IsValidated = true, + Location = nameOrLocation, + Name = nameOrLocation, + Trusted = true, + }; + } + } catch { + } + + try { + // is the given value a directory? + if (Directory.Exists(nameOrLocation)) { + Debug(Resources.Messages.SourceIsADirectory, nameOrLocation); + return new PackageSource() { + Request = this, + IsRegistered = false, + IsValidated = true, + Location = nameOrLocation, + Name = nameOrLocation, + Trusted = true, + }; + } + } catch { + } + + if (Uri.IsWellFormedUriString(nameOrLocation, UriKind.Absolute)) { + var uri = new Uri(nameOrLocation, UriKind.Absolute); + if (!SupportedSchemes.Contains(uri.Scheme.ToLowerInvariant())) { + WriteError(ErrorCategory.InvalidArgument, uri.ToString(), Constants.Messages.UriSchemeNotSupported, uri); + return null; + } + + // this is an URI, and it looks like one type that we support + if (SkipValidate.Value || NuGetPathUtility.ValidateSourceUri(SupportedSchemes, uri, this)) { + var uriSource = new PackageSource { + Request = this, + IsRegistered = false, + IsValidated = !SkipValidate.Value, + Location = nameOrLocation, + Name = nameOrLocation, + Trusted = false, + }; + + return uriSource; + } + } + + WriteError(ErrorCategory.InvalidArgument, nameOrLocation, Constants.Messages.UnableToResolveSource, nameOrLocation); + return null; + } + + private bool TryParseFastPath(string fastPath, out string source, out string id, out string version, out string[] sources) + { + var match = _regexFastPath.Match(fastPath); + source = match.Success ? match.Groups["source"].Value.FromBase64() : null; + id = match.Success ? match.Groups["id"].Value.FromBase64() : null; + version = match.Success ? match.Groups["version"].Value.FromBase64() : null; + var srcs = match.Success ? match.Groups["sources"].Value : string.Empty; + sources = srcs.Split('|').Select(each => each.FromBase64()).Where(each => !string.IsNullOrWhiteSpace(each)).ToArray(); + return match.Success; + } + + private bool LocationCloseEnoughMatch(string givenLocation, string knownLocation) { + if (givenLocation.Equals(knownLocation, StringComparison.OrdinalIgnoreCase)) { + return true; + } + + // make trailing slashes consistent + if (givenLocation.TrimEnd('/').Equals(knownLocation.TrimEnd('/'), StringComparison.OrdinalIgnoreCase)) { + return true; + } + + // and trailing backslashes + if (givenLocation.TrimEnd('\\').Equals(knownLocation.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase)) { + return true; + } + return false; + } + + private IEnumerable FilterOnVersion(IEnumerable pkgs, string requiredVersion, string minimumVersion, string maximumVersion, bool minInclusive = true, bool maxInclusive = true) { + if (!String.IsNullOrWhiteSpace(requiredVersion)) + { + pkgs = pkgs.Where(each => each.Version == new SemanticVersion(requiredVersion)); + } else { + if (!String.IsNullOrWhiteSpace(minimumVersion)) + { + // if minInclusive, then use >= else use > + if (minInclusive) + { + pkgs = pkgs.Where(each => each.Version >= new SemanticVersion(minimumVersion)); + } + else + { + pkgs = pkgs.Where(each => each.Version > new SemanticVersion(minimumVersion)); + } + } + + if (!String.IsNullOrWhiteSpace(maximumVersion)) + { + // if maxInclusive, then use < else use <= + if (maxInclusive) + { + pkgs = pkgs.Where(each => each.Version <= new SemanticVersion(maximumVersion)); + } + else + { + pkgs = pkgs.Where(each => each.Version < new SemanticVersion(maximumVersion)); + } + } + } + + return pkgs; + } + + private IEnumerable FilterOnName(IEnumerable pkgs, string searchTerm, bool useWildCard) + { + if (useWildCard) + { + // Applying the wildcard pattern matching + const WildcardOptions wildcardOptions = WildcardOptions.CultureInvariant | WildcardOptions.IgnoreCase; + var wildcardPattern = new WildcardPattern(searchTerm, wildcardOptions); + + return pkgs.Where(p => wildcardPattern.IsMatch(p.Id)); + + } else { + return pkgs.Where(each => each.Id.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) > -1); + } + } + + internal IEnumerable FilterOnTags(IEnumerable pkgs) + { + if (FilterOnTag == null || FilterOnTag.Value.Length == 0) + { + return pkgs; + } + + //Tags should be performed as *AND* intead of *OR" + //For example -FilterOnTag:{ "A", "B"}, the returned package should have both A and B. + return pkgs.Where(pkg => FilterOnTag.Value.All( + tagFromUser => + { + if (string.IsNullOrWhiteSpace(pkg.Tags)) + { + // return all packages if a package has no tags. + return true; + } + var tagArray = pkg.Tags.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + return tagArray.Any(tagFromPackage => tagFromPackage.EqualsIgnoreCase(tagFromUser)); + } + )); + } + + private IEnumerable FilterOnContains(IEnumerable pkgs) { + if (string.IsNullOrWhiteSpace(Contains.Value)) + { + return pkgs; + } + return pkgs.Where(each => each.Description.IndexOf(Contains.Value, StringComparison.OrdinalIgnoreCase) > -1 || + each.Id.IndexOf(Contains.Value, StringComparison.OrdinalIgnoreCase) > -1); + } + + internal IEnumerable SearchForPackages(string name) + { + var sources = SelectedSources.AsParallel().WithMergeOptions(ParallelMergeOptions.NotBuffered); + + return sources.SelectMany(source => SearchForPackages(source, name)); + } + + /// + /// Search the entire repository for the packages + /// + /// Package Source + /// Package name + /// + private IEnumerable SearchForPackages(PackageSource source, string name) + { + try + { + Debug(Resources.Messages.DebugInfoCallMethod3, "NuGetRequest", "SearchForPackages", name); + + // no repository found then returns nothing + if (source.Repository == null) + { + return Enumerable.Empty(); + } + + var isNameContainsWildCard = false; + var searchTerm = Contains.Value ?? string.Empty; + + // Deal with two cases here: + // 1. The package name contains wildcards like "Find-Package -name JQ*". We search based on the name. + // 2. A user does not provide the Name parameter, + + // A user provides name with wildcards. We will use name as the SearchTerm while Contains and Tag will be used + // for filtering on the results. + if (!String.IsNullOrWhiteSpace(name) && WildcardPattern.ContainsWildcardCharacters(name)) + { + isNameContainsWildCard = true; + + // NuGet does not support PowerShell/POSIX style wildcards and supports only '*' in searchTerm + // Replace the range from '[' - to ']' with * and ? with * then wildcard pattern is applied on the results + var tempName = name; + var squareBracketPattern = Regex.Escape("[") + "(.*?)]"; + foreach (Match match in Regex.Matches(tempName, squareBracketPattern)) { + tempName = tempName.Replace(match.Value, "*"); + } + + //As the nuget does not support wildcard, we remove '?', '*' wildcards from the search string, and then + //looking for the longest string in the given string as a keyword for the searching in the repository. + //A sample case will be something like find-package sql*Compact*. + + //When the AllVersions property exists, the query like the following containing the wildcards does not work. We need to remove the wild cards and + //replace it with the longest string searhc. + //http://www.powershellgallery.com/api/v2/Search()?$orderby=DownloadCount%20desc,Id&searchTerm='tsdprovi*'&targetFramework=''&includePrerelease=false + + if ((!String.IsNullOrWhiteSpace(name) && source.Location.IndexOf("powershellgallery.com", StringComparison.OrdinalIgnoreCase) == -1) + || (AllVersions.Value)) + { + //get rid of wildcard and search the longest string in the given name for nuget.org + tempName = tempName.Split('?', '*').OrderBy(namePart => namePart.Length).Last(); + } + + //Deal with a case when a user type Find-Package * + if (String.Equals(tempName, "*", StringComparison.OrdinalIgnoreCase)) { + //We use '' instead of "*" because searchterm='*' does not return the entire repository. + tempName = string.Empty; + } + + searchTerm = tempName; + } + + // Add the Tags for the search. + if (FilterOnTag != null) + { + // E.g. searchTerm = "tag:dscresource_xFirefox tag:command_Start-Process" + searchTerm = FilterOnTag.Value.Where(tag => !string.IsNullOrWhiteSpace(tag)).Aggregate(searchTerm, (current, tag) => current + " tag:" + tag); + } + + Verbose(Resources.Messages.SearchingRepository, source.Repository.Source, searchTerm); + + // Handling case where a user does not provide Name parameter + var pkgs = source.Repository.Search(searchTerm, this); + + if (!String.IsNullOrWhiteSpace(name)) + { + //Filter on the results. This is needed because we replace [...] regex in the searchterm at the begining of this method. + pkgs = FilterOnName(pkgs, name, isNameContainsWildCard); + } + + pkgs = FilterOnContains(pkgs); + + var pkgsItem = pkgs.Select(pkg => new PackageItem + { + Package = pkg, + PackageSource = source, + FastPath = MakeFastPath(source, pkg.Id, pkg.Version.ToString()) + }); + + return pkgsItem; + } + catch (Exception e) + { + e.Dump(this); + Warning(e.Message); + return Enumerable.Empty(); + } + } + } +} diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Package/IPackage.cs b/src/Microsoft.PackageManagement.NuGetProvider/Package/IPackage.cs new file mode 100644 index 000000000..3ee4ae426 --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Package/IPackage.cs @@ -0,0 +1,46 @@ +namespace Microsoft.PackageManagement.NuGetProvider +{ + using System; + using System.Collections.Generic; + + public interface IPackage : IPackageName + { + //NugetClient:IPackage + Uri ReportAbuseUrl { get; } + Uri LicenseReportUrl { get; } + long DownloadCount { get; } + long PackageSize { get; } + long VersionDownloadCount { get; } + bool IsAbsoluteLatestVersion { get; } + bool IsLatestVersion { get; } + bool Listed { get; } + DateTimeOffset? Published { get; } + DateTimeOffset? Created { get; } + DateTimeOffset? LastUpdated { get; } + DateTimeOffset? LastEdited { get; } + string FullFilePath { get; set; } + + //NugetClient:IPackageMetadata + string Title { get; } + IEnumerable Authors { get; } + IEnumerable Owners { get; } + Uri IconUrl { get; } + Uri LicenseUrl { get;} + Uri ProjectUrl { get; } + Uri GalleryDetailsUrl { get; } + bool RequireLicenseAcceptance { get; } + bool DevelopmentDependency { get; } + string LicenseNames { get; } + string Description { get; } + string Summary { get; } + string ContentSrcUrl { get;} + string ReleaseNotes { get; } + string Language { get; } + string Tags { get; } + string Copyright { get; } + Version MinClientVersion { get; } + List DependencySetList { get; } + string PackageHash { get; } + string PackageHashAlgorithm { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Package/IPackageName.cs b/src/Microsoft.PackageManagement.NuGetProvider/Package/IPackageName.cs new file mode 100644 index 000000000..0b0b16ebe --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Package/IPackageName.cs @@ -0,0 +1,9 @@ +namespace Microsoft.PackageManagement.NuGetProvider +{ + using Microsoft.PackageManagement.Provider.Utility; + public interface IPackageName + { + string Id { get; } + SemanticVersion Version { get; } + } +} diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Package/PackageBase.cs b/src/Microsoft.PackageManagement.NuGetProvider/Package/PackageBase.cs new file mode 100644 index 000000000..a2c2ecf2f --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Package/PackageBase.cs @@ -0,0 +1,172 @@ +namespace Microsoft.PackageManagement.NuGetProvider +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Microsoft.PackageManagement.Provider.Utility; + + public class PackageBase : IPackage + { + private long _downloadCount = -1; + private long _versionDownloadCount = -1; + private long _packageSize = -1; + + public PackageBase() + { + AdditionalProperties = new Dictionary(); + } + + string IPackageName.Id + { + get + { + return Id; + } + } + + SemanticVersion IPackageName.Version + { + get + { + if (Version != null) + { + return new SemanticVersion(Version); + } + return null; + } + } + + public bool IsAbsoluteLatestVersion { get; set; } + + public bool IsLatestVersion { get; set;} + + public bool Listed{ get; set;} + + public string FullFilePath { get; set; } + + public DateTimeOffset? Published { get; set;} + + public DateTimeOffset? Created { get; set; } + + public DateTimeOffset? LastUpdated { get; set; } + + public DateTimeOffset? LastEdited { get; set; } + + public string Title{ get; set;} + + public Uri IconUrl{ get; set;} + + public Uri LicenseUrl{ get; set;} + + public Uri ProjectUrl{ get; set;} + + public Uri GalleryDetailsUrl { get; set; } + + public bool RequireLicenseAcceptance{ get; set;} + + public bool DevelopmentDependency{ get; set;} + + public string Description{ get; set;} + + public string Summary{ get; set;} + + public string ContentSrcUrl { get; set; } + + public string ReleaseNotes{ get; set;} + + public string Language{ get; set;} + + public string Tags{ get; set;} + + public string Copyright{ get; set;} + + public string LicenseNames { get; set; } + + public Version MinClientVersion{ get; set;} + + public long DownloadCount + { + get + { + return _downloadCount; + } + set + { + _downloadCount = value; + } + } + + public Uri ReportAbuseUrl{ get; set; } + + public Uri LicenseReportUrl { get; set; } + + public long VersionDownloadCount + { + get + { + return _versionDownloadCount; + } + set + { + _versionDownloadCount = value; + } + } + + public long PackageSize + { + get + { + return _packageSize; + } + set + { + _packageSize = value; + } + } + + IEnumerable IPackage.Authors + { + get + { + if (String.IsNullOrWhiteSpace(Authors)) + { + return Enumerable.Empty(); + } + return Authors.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + } + + IEnumerable IPackage.Owners + { + get + { + if (String.IsNullOrWhiteSpace(Owners)) + { + return Enumerable.Empty(); + } + return Owners.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + } + } + + internal string Id { get; set; } + + internal string Version { get; set; } + + internal string Authors { get; set; } + + internal string Owners { get; set; } + + public string GetFullName() + { + return Id + " " + Version; + } + + public List DependencySetList { get; set; } + + public string PackageHashAlgorithm { get; set; } + + public string PackageHash { get; set; } + + public Dictionary AdditionalProperties { get; set; } + } +} diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Package/PackageDependency.cs b/src/Microsoft.PackageManagement.NuGetProvider/Package/PackageDependency.cs new file mode 100644 index 000000000..fa5e1c1d5 --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Package/PackageDependency.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.PackageManagement.Provider.Utility; + +namespace Microsoft.PackageManagement.NuGetProvider +{ + /// + /// This class represents PackageDependency + /// + public sealed class PackageDependency + { + /// + /// Name of the dependency + /// + public string Id { get; set; } + + /// + /// The dependency version required. this may include min and max version + /// + public DependencyVersion DependencyVersion { get; set; } + } +} diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Package/PackageDependencySet.cs b/src/Microsoft.PackageManagement.NuGetProvider/Package/PackageDependencySet.cs new file mode 100644 index 000000000..00cd9a3fb --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Package/PackageDependencySet.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PackageManagement.NuGetProvider +{ + /// + /// This class represents a set of dependencies within a framework + /// + public sealed class PackageDependencySet + { + /// + /// The target framework of this dependency set + /// + public string TargetFramework { get; set; } + + /// + /// The list of dependencies + /// + public List Dependencies { get; set; } + } +} diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Package/PackageItem.cs b/src/Microsoft.PackageManagement.NuGetProvider/Package/PackageItem.cs new file mode 100644 index 000000000..146050c07 --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Package/PackageItem.cs @@ -0,0 +1,147 @@ +namespace Microsoft.PackageManagement.NuGetProvider +{ + using System; + using System.Collections.Generic; + using System.IO; + + internal class PackageItem + { + private string _fullPath; + + internal IPackage Package { get; set; } + + internal PackageSource PackageSource { get; set; } + + internal string FastPath { get; set; } + + internal bool IsPackageFile { get; set; } + + internal string[] Sources { get; set; } + + internal string Id + { + get + { + if (Package != null) + { + return Package.Id; + } + return null; + } + + } + + internal string InstalledDirectory + { + get + { + try + { + // if this package file is in a folder with the same name, + // we'll consider that 'installed' + if (IsPackageFile) + { + var dir = Path.GetDirectoryName(PackageSource.Location); + if (!string.IsNullOrWhiteSpace(dir)) + { + var dirName = Path.GetFileName(dir); + var name = Path.GetFileNameWithoutExtension(PackageSource.Location); + + //There is a case where we keep .nuspec + //c:\...\jQuery.2.1.4 + // jQuery.nuspec + //dirName = jQuery.2.1.4 + //name = jQuery + //conduct case insensitive comparison between name and dirName + if (!string.IsNullOrEmpty(name) && (dirName.IndexOf(name, StringComparison.OrdinalIgnoreCase) >= 0) && Directory.Exists(dir)) + { + return dir; + } + } + } + } + catch + { + } + return null; + } + } + + internal bool IsInstalled + { + get + { + return !string.IsNullOrWhiteSpace(InstalledDirectory); + } + } + + internal string Version + { + get + { + if ((Package != null) && (Package.Version != null)) + { + return Package.Version.ToString(); + } + return null; + } + } + + //internal string FullName + //{ + // get + // { + // if (Package != null) + // { + // return Package.GetFullName(); + // } + + // return string.Format("{0}.{1}", Id, Version); + // } + //} + + internal string PackageFilename + { + get + { + if (IsPackageFile) + { + return Path.GetFileName(PackageSource.Location); + } + + return Id + "." + Version + NuGetConstant.PackageExtension; + } + } + + internal string FullPath + { + get + { + if (IsPackageFile) + { + return Path.GetFileName(PackageSource.Location); + } + return _fullPath; + } + set + { + _fullPath = value; + } + } + } + + //Comparer for packges using version and id + internal class PackageItemComparer : IEqualityComparer + { + public bool Equals(PackageItem packageOne, PackageItem packageTwo) + { + return String.Equals(packageOne.Id, packageTwo.Id, StringComparison.OrdinalIgnoreCase) && String.Equals(packageOne.Version, packageTwo.Version, StringComparison.OrdinalIgnoreCase); + } + + public int GetHashCode(PackageItem obj) + { + return (String.IsNullOrWhiteSpace(obj.Id) ? String.Empty : obj.Id).GetHashCode() * 31 + + (String.IsNullOrWhiteSpace(obj.Version) ? String.Empty : obj.Version).GetHashCode(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Package/PackageName.cs b/src/Microsoft.PackageManagement.NuGetProvider/Package/PackageName.cs new file mode 100644 index 000000000..bf6772763 --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Package/PackageName.cs @@ -0,0 +1,58 @@ +namespace Microsoft.PackageManagement.NuGetProvider +{ + using System; + using Microsoft.PackageManagement.Provider.Utility; + + public class PackageName : IPackageName, IEquatable + { + private readonly string _packageId; + private readonly SemanticVersion _version; + + public PackageName(string packageId, SemanticVersion version) + { + _packageId = packageId; + _version = version; + } + + public string Id + { + get { return _packageId; } + } + + public SemanticVersion Version + { + get + { + return _version; + } + } + + public string Name + { + get + { + return _packageId + "." + _version; + } + } + + public bool Equals(PackageName other) + { + if (other == null) { + return ReferenceEquals(_packageId, null); + } + + return _packageId.Equals(other._packageId, StringComparison.OrdinalIgnoreCase) && + _version.Equals(other._version); + } + + public override int GetHashCode() + { + return _packageId.GetHashCode() * 3137 + _version.GetHashCode(); + } + + public override string ToString() + { + return _packageId + " " + _version; + } + } +} diff --git a/src/Microsoft.PackageManagement.NuGetProvider/PackageSource.cs b/src/Microsoft.PackageManagement.NuGetProvider/PackageSource.cs new file mode 100644 index 000000000..ddaaddbe4 --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/PackageSource.cs @@ -0,0 +1,67 @@ + +using Microsoft.PackageManagement.Provider.Utility; + +namespace Microsoft.PackageManagement.NuGetProvider +{ + using System; + using System.Net; + using System.IO; + + internal class PackageSource + { + private IPackageRepository _repository; + + //Parameters must be filled in during the instantiation. + + internal string Name { get; set; } + + internal string Location { get; set; } + + internal bool Trusted { get; set; } + + internal bool IsRegistered { get; set; } + + internal bool IsValidated { get; set; } + + internal NuGetRequest Request { get; set; } + + internal IPackageRepository Repository + { + get + { + if (!IsSourceAFile) + { + return _repository ?? (_repository = PackageRepositoryFactory.Default.CreateRepository(Location, Request)); + } + return null; + } + } + + internal bool IsSourceAFile + { + get + { + try + { + if (!string.IsNullOrWhiteSpace(Location) && ((!Uri.IsWellFormedUriString(Location, UriKind.Absolute) || new Uri(Location).IsFile) && File.Exists(Location))) + { + return true; + } + } + catch + { + // no worries. + } + return false; + } + } + + internal string Serialized + { + get + { + return Location.ToBase64(); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Repository/HttpClientPackageRepository.cs b/src/Microsoft.PackageManagement.NuGetProvider/Repository/HttpClientPackageRepository.cs new file mode 100644 index 000000000..fa65b1f6a --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Repository/HttpClientPackageRepository.cs @@ -0,0 +1,461 @@ +namespace Microsoft.PackageManagement.NuGetProvider { + using System; + using System.Net; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Threading.Tasks; + using System.Xml.Linq; + using Resources; + using System.Collections.Concurrent; + using System.Globalization; + using System.Security; + using Microsoft.PackageManagement.Provider.Utility; + + /// + /// Package repository for downloading data from remote galleries + /// + internal class HttpClientPackageRepository : IPackageRepository + { + private readonly string _nugetFindPackageIdQueryFormat; + private readonly string _queryUri; + + /// + /// Ctor's + /// + /// The nuget request object + /// Packagesource location + internal HttpClientPackageRepository(string queryUrl, NuGetRequest request) + { + // Validate the url + + Uri newUri; + Uri validatedUri = null; + + if (Uri.TryCreate(queryUrl, UriKind.Absolute, out newUri)) + { + validatedUri = NuGetPathUtility.ValidateUri(newUri, request); + } + + if (validatedUri == null) + { + throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, Messages.InvalidQueryUrl, queryUrl)); + } + + queryUrl = validatedUri.AbsoluteUri; + + // if a query is http://www.nuget.org/api/v2 then we add / to the end + if (!queryUrl.EndsWith("/", StringComparison.OrdinalIgnoreCase)) + { + queryUrl = String.Concat(queryUrl, "/"); + } + + _queryUri = queryUrl; + + //we are constructing the url query like http://www.nuget.org/api/v2/FindPackagesById()?id='JQuery' + _nugetFindPackageIdQueryFormat = PathUtility.UriCombine(_queryUri, NuGetConstant.FindPackagesById); + } + + /// + /// Package source location + /// + public string Source { + get { + // Package source Uri + return _queryUri; + } + } + + /// + /// True if the packagesource is a file repository + /// + public bool IsFile + { + get + { + //false because this is not a local file repository + return false; + } + } + + /// + /// Find-Package + /// + /// package Id + /// package version + /// + /// + public IPackage FindPackage(string packageId, SemanticVersion version, NuGetRequest request) + { + if (string.IsNullOrWhiteSpace(packageId)) { + return null; + } + + request.Debug(Messages.DebugInfoCallMethod3, "HttpClientPackageRepository", "FindPackage", packageId); + + var query = packageId.MakeFindPackageByIdQuery(_nugetFindPackageIdQueryFormat); + + var packages = NuGetClient.FindPackage(query, request); + + //Usually versions has a limited number, ToArray should be ok. + var versions = version.GetComparableVersionStrings().ToArray(); + + //Will only enumerate oackages once + return packages.FirstOrDefault(package => packageId.Equals(package.Id, StringComparison.OrdinalIgnoreCase) && versions.Contains(package.Version,StringComparer.OrdinalIgnoreCase)); + } + + /// + /// Find-Package bases on the given package Id + /// + /// Package Id + /// + /// + public IEnumerable FindPackagesById(string packageId, NuGetRequest request){ + + request.Debug(Messages.DebugInfoCallMethod3, "HttpClientPackageRepository", "FindPackagesById", packageId); + + var query = packageId.MakeFindPackageByIdQuery(_nugetFindPackageIdQueryFormat); + + //request.Verbose(query.ToString()); + + return NuGetClient.FindPackage(query, request); + } + + /// + /// Search the entire repository for the case when a user does not provider package name or uses wildcards in the name. + /// + /// + /// + /// + public IEnumerable Search(string searchTerm, NuGetRequest nugetRequest) + { + if (nugetRequest == null) + { + yield break; + } + + nugetRequest.Debug(Messages.DebugInfoCallMethod3, "HttpClientPackageRepository", "Search", searchTerm); + + var searchQuery = searchTerm.MakeSearchQuery(_queryUri, nugetRequest.AllowPrereleaseVersions.Value, nugetRequest.AllVersions.Value); + + foreach (var pkg in SendRequest(searchQuery, nugetRequest)) + { + yield return pkg; + } + } + + /// + /// Send the request to the server with buffer size to account for the case where there are more data + /// that we need to fetch + /// + /// + /// + /// + public static IEnumerable SendRequest(string query, NuGetRequest request) + { + const int bufferSize = 40; + // number of threads sending the requests + const int numberOfSenders = 4; + + var startPoint = 0; + var tasks = new List>(); + + bool stopSending = false; + object stopLock = new Object(); + + // Send one request first + + // this initial query is of the form http://www.nuget.org/api/v2/FindPackagesById()?id='jquery'&$skip={0}&$top={1} + UriBuilder initialQuery = new UriBuilder(query.InsertSkipAndTop()); + + PackageBase firstPackage = null; + + // Send out an initial request + // we send out 1 initial request first to check for redirection and check whether repository supports odata + using (Stream stream = NuGetClient.InitialDownloadDataToStream(initialQuery, startPoint, bufferSize, request)) + { + if (stream == null) + { + yield break; + } + + XDocument document = XmlUtility.LoadSafe(stream, ignoreWhiteSpace: true); + + var entries = document.Root.ElementsNoNamespace("entry").ToList(); + + // If the initial request has different number of entries than the buffer size, return it because this means the server + // does not understand odata request or there is no more data. in the former case, we have to stop to prevent infinite loop + if (entries.Count != bufferSize) + { + request.Debug(Messages.PackagesReceived, entries.Count); + stopSending = true; + } + + foreach (XElement entry in entries) + { + var package = new PackageBase(); + + // set the first package of the request. this is used later to verify that the case when the number of packages in the repository + // is the same as the buffer size and the repository does not support odata query. in that case, we want to check whether the first package + // exists anywhere in the second call. if it is, then we cancel the request (this is to prevent infinite loop) + if (firstPackage == null) + { + firstPackage = package; + } + + PackageUtility.ReadEntryElement(ref package, entry); + yield return package; + } + } + + if (stopSending || request.IsCanceled) + { + yield break; + } + + // To avoid more redirection (for example, if the initial query is nuget.org, it will be changed to www.nuget.org + + query = initialQuery.Uri.ToString(); + + // Sending the initial requests + for (var i = 0; i < numberOfSenders; i++) + { + // Update the start point to fetch the packages + startPoint += bufferSize; + + // Get the query + var newQuery = string.Format(query, startPoint, bufferSize); + + // Send it + tasks.Add(Task.Factory.StartNew(() => + { + Stream items = NuGetClient.DownloadDataToStream(newQuery, request); + return items; + })); + } + + //Wait for the responses, parse the data, and send to the user + while (tasks.Count > 0) + { + + //Cast because the compiler warning: Co-variant array conversion from Task[] to Task[] can cause run-time exception on write operation. + var index = Task.WaitAny(tasks.Cast().ToArray()); + + using (Stream stream = tasks[index].Result) + { + if (stream == null) + { + yield break; + } + + XDocument document = XmlUtility.LoadSafe(stream, ignoreWhiteSpace: true); + + var entries = document.Root.ElementsNoNamespace("entry").ToList(); + + if (entries.Count < bufferSize) + { + request.Debug(Messages.PackagesReceived, entries.Count); + lock (stopLock) + { + stopSending = true; + } + } + + foreach (XElement entry in entries) + { + var package = new PackageBase(); + + PackageUtility.ReadEntryElement(ref package, entry); + + if (firstPackage != null) + { + // check whether first package in the first request exists anywhere in the second request + if (string.Equals(firstPackage.GetFullName(), package.GetFullName(), StringComparison.OrdinalIgnoreCase) + && string.Equals(firstPackage.Version, package.Version, StringComparison.OrdinalIgnoreCase)) + { + lock (stopLock) + { + stopSending = true; + } + + break; + } + } + + yield return package; + } + + // we only needs to check for the existence of the first package in the second request. don't need to do for subsequent request + if (firstPackage != null) + { + firstPackage = null; + } + } + + // checks whether we should stop sending requests + if (!stopSending && !request.IsCanceled) + { + // Make sure nobody else is updating the startPoint + lock (stopLock) + { + // update the startPoint + startPoint += bufferSize; + } + // Make a new request with the new startPoint + var newQuery = string.Format(query, startPoint, bufferSize); + + //Keep sending a request + tasks[index] = (Task.Factory.StartNew(searchQuery => + { + var items = NuGetClient.DownloadDataToStream(searchQuery.ToStringSafe(), request); + return items; + }, newQuery)); + + } + else + { + if (request.IsCanceled) + { + request.Warning(Messages.RequestCanceled, "HttpClientPackageRepository", "SendRequest"); + //stop sending request to the remote server + stopSending = true; + } + + tasks.RemoveAt(index); + } + } + + } + + #region SkipTokenCode + // This code is commented out because powershellgallery has a bug with skip token. We can uncomment it once that is fixed. + ///// + ///// Create a new task that will automatically create a task and add it to taskCollection + ///// if there are more links to be downloaded. Otherwise, it will signal to the taskCollection that + ///// we are not expecting any more results. + ///// The task will also add any packages that it produced to packageCollection + ///// + ///// + ///// + ///// + ///// + //private static Task CreateDownloadTask(string query, BlockingCollection> taskCollection, BlockingCollection packageCollection,Request request) + //{ + // Task taskStream = Task.Factory.StartNew(searchQuery => + // { + // var items = NuGetClient.DownloadDataToStream(searchQuery.ToStringSafe(), request); + // return items; + // }, query); + + // // After the task is done, we check whether we should create a new task + // taskStream.ContinueWith(streamTask => + // { + // using (Stream stream = streamTask.Result) + // { + // XDocument document = XmlUtility.LoadSafe(stream, ignoreWhiteSpace: true); + + // // find the xelement of the form + // XElement next = document.Root.Elements().FirstOrDefault(e => String.Equals(e.Name.LocalName, "link", StringComparison.OrdinalIgnoreCase) + // && e.Attribute("rel") != null + // && String.Equals(e.Attribute("rel").Value, "next", StringComparison.OrdinalIgnoreCase) + // && e.Attribute("href") != null); + + // // If there is no next link or the request is cancelled, stop sending the request + // if (next == null || request.IsCanceled) + // { + // // call completeadding to signal that there won't be anymore request + // taskCollection.CompleteAdding(); + // } + // else + // { + // var newQuery = next.Attribute("href").Value; + // taskCollection.Add(CreateDownloadTask(newQuery, taskCollection, packageCollection, request)); + // } + + // foreach (XElement entry in document.Root.ElementsNoNamespace("entry")) + // { + // var package = new PackageBase(); + + // PackageUtility.ReadEntryElement(ref package, entry); + // packageCollection.Add(package); + // } + + // if (next == null || request.IsCanceled) + // { + // packageCollection.CompleteAdding(); + // } + // } + // }); + + // return taskStream; + //} + + ///// + ///// Send the request to the server. We check whether the response has a next link + ///// to account for the case where there are more data that we need to fetch + ///// + ///// + ///// + ///// + //public static IEnumerable SendRequest(string query, Request request) + //{ + // var tasks = new List>(); + + // // A blocking collection of task stream. + // BlockingCollection> taskCollection = new BlockingCollection>(); + // // A blocking collection of packages. + // BlockingCollection packageCollection = new BlockingCollection(); + + // // Populate the first task + // Task firstTask = CreateDownloadTask(query, taskCollection, packageCollection, request); + + // while (!taskCollection.IsCompleted) + // { + // Task streamTask = null; + + // try + // { + // streamTask = taskCollection.Take(); + // } + // catch (InvalidOperationException) { } + + // // Try to yield package from packageCollection + // while (!packageCollection.IsCompleted) + // { + // PackageBase package = null; + + // try + // { + // package = packageCollection.Take(); + // } + // catch (InvalidOperationException) { } + + // if (package != null) + // { + // yield return package; + // } + // } + // } + + // // There may be packages in packageCollection + // // Try to yield package from packageCollection + // while (!packageCollection.IsCompleted) + // { + // PackageBase package = null; + + // try + // { + // package = packageCollection.Take(); + // } + // catch (InvalidOperationException) { } + + // if (package != null) + // { + // yield return package; + // } + // } + //} + #endregion + } +} + diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Repository/IPackageRepository.cs b/src/Microsoft.PackageManagement.NuGetProvider/Repository/IPackageRepository.cs new file mode 100644 index 000000000..8373574b8 --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Repository/IPackageRepository.cs @@ -0,0 +1,38 @@ +namespace Microsoft.PackageManagement.NuGetProvider +{ + using System.Collections.Generic; + using System.Linq; + using Microsoft.PackageManagement.Provider.Utility; + + public interface IPackageRepository + { + /// + /// Package source location + /// + string Source { get; } + + /// + /// True if a file repository. + /// + bool IsFile { get; } + + /// + /// Finds packages that match the exact Id and version. + /// + /// The package if found, null otherwise. + IPackage FindPackage(string packageId, SemanticVersion version, NuGetRequest request); + + /// + /// Returns a sequence of packages with the specified id. + /// + IEnumerable FindPackagesById(string packageId, NuGetRequest request); + + /// + /// Nuget V2 metadata supports a method 'Search'. It takes three parameters, searchTerm, targetFramework, and includePrerelease. + /// + /// search uri + /// + /// + IEnumerable Search(string searchTerm, NuGetRequest request); + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Repository/IPackageRepositoryFactory.cs b/src/Microsoft.PackageManagement.NuGetProvider/Repository/IPackageRepositoryFactory.cs new file mode 100644 index 000000000..5e2199797 --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Repository/IPackageRepositoryFactory.cs @@ -0,0 +1,8 @@ +namespace Microsoft.PackageManagement.NuGetProvider +{ + + public interface IPackageRepositoryFactory + { + IPackageRepository CreateRepository(string packageSource, NuGetRequest request); + } +} diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Repository/LocalPackageRepository.cs b/src/Microsoft.PackageManagement.NuGetProvider/Repository/LocalPackageRepository.cs new file mode 100644 index 000000000..64b0a793a --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Repository/LocalPackageRepository.cs @@ -0,0 +1,369 @@ +namespace Microsoft.PackageManagement.NuGetProvider +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Linq; + using Microsoft.PackageManagement.Provider.Utility; + + /// + /// Package repository for downloading data from file repositories + /// + internal class LocalPackageRepository : IPackageRepository { + private readonly string _path; + + /// + /// Ctor's + /// + /// + /// + public LocalPackageRepository(string physicalPath, NuGetRequest request) { + _path = physicalPath; + } + + /// + /// Package source location + /// + public string Source { + get { + return _path; + } + } + + /// + /// Finding the packages in the file repository + /// + /// Delegate function which is actually finding a package + /// Package Id + /// File repository path + /// + /// + private static IEnumerable GetPackages(Func openPackage, + string packageId, + IEnumerable packagePaths, + Request request) + { + request.Debug(Resources.Messages.DebugInfoCallMethod3, "LocalPackageRepository", "GetPackages", packageId); + + foreach (var path in packagePaths) + { + IPackage package = null; + try { + package = GetPackage(openPackage, path, request); + } catch (InvalidOperationException ex) { + // ignore error for unzipped packages (nuspec files). + if (!string.Equals(NuGetConstant.ManifestExtension, Path.GetExtension(path), StringComparison.OrdinalIgnoreCase)) { + request.Verbose(ex.Message); + throw; + } + } + + if (package != null && package.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase)) { + yield return package; + } + } + } + + /// + /// Finding the package in the file repository + /// + /// Delegate function which is actually finding a package + /// File repository path + /// + /// + private static IPackage GetPackage(Func openPackage, string path, Request request) + { + //Create the package + IPackage package = openPackage(path, request); + + return package; + } + + /// + /// A delegate used for finding the package. + /// + /// File repository path + /// + /// + private static IPackage OpenPackage(string path, Request request) { + + request.Debug(Resources.Messages.DebugInfoCallMethod3, "LocalPackageRepository", "OpenPackage", path); + + if (!File.Exists(path)) { + request.Warning(Resources.Messages.FileNotFound, path,"LocalPackageRepository::OpenPackage"); + return null; + } + + //deal with .nupkg + if (string.Equals(Path.GetExtension(path), NuGetConstant.PackageExtension, StringComparison.OrdinalIgnoreCase)) { + PackageBase package; + try { + package = ProcessZipPackage(path); + } catch (Exception ex) { + request.Verbose(ex.Message); + throw; + } + + // Set the last modified date on the package + package.Published = FileUtility.GetLastModified(path); + + // We assume local files in the local repository are all latest version + package.IsAbsoluteLatestVersion = true; + package.IsLatestVersion = true; + package.FullFilePath = path; + + return package; + } + + return null; + } + + /// + /// Unzip the package and create PackageImpl object + /// + /// The .nupkg file path + /// + private static PackageBase ProcessZipPackage(string nupkgPath) { + + string packageName = Path.GetFileNameWithoutExtension(nupkgPath); + packageName = PackageUtility.GetPackageNameWithoutVersionInfo(packageName); + PackageBase package = PackageUtility.DecompressFile(nupkgPath, packageName); + + return package; + } + + /// + /// Get the .nupkg files + /// + /// The file filter to be applied while finding the .nupkg files + /// + private IEnumerable GetPackageFiles(string filter = null) { + filter = filter ?? "*" + NuGetConstant.PackageExtension; + + // Check for package files one level deep. We use this at package install time + // to determine the set of installed packages. Installed packages are copied to + // {id}.{version}\{packagefile}.{extension}. + foreach (var dir in FileUtility.GetDirectories(_path)) + //foreach (var dir in FileSystem.GetDirectories(String.Empty)) + { + foreach (var path in FileUtility.GetFiles(dir, filter, recursive: false)) { + yield return path; + } + } + + // Check top level directory + foreach (var path in FileUtility.GetFiles(_path, filter, recursive: false)) { + yield return path; + } + } + + /// + /// Find-Package + /// + /// Package id + /// Package Name + /// + /// + public virtual IPackage FindPackage(string packageId, SemanticVersion version, NuGetRequest request) + { + return FindPackage(OpenPackage, packageId, version, request); + } + + /// + /// Find-Package + /// + /// Delegate function which is actually finding a package + /// Package Id + /// Package version + /// + /// + private IPackage FindPackage(Func openPackage, string packageId, SemanticVersion version, NuGetRequest nugetRequest) + { + if (nugetRequest == null) { + return null; + } + + nugetRequest.Debug(Resources.Messages.SearchingRepository, "FindPackage", packageId); + + var lookupPackageName = new PackageName(packageId, version); + + //handle file cache here if we want to support local package cache later + + // Lookup files which start with the name "." and attempt to match it with all possible version string combinations (e.g. 1.2.0, 1.2.0.0) + // before opening the package. To avoid creating file name strings, we attempt to specifically match everything after the last path separator + // which would be the file name and extension. + return (from path in GetPackageLookupPaths(packageId, version) + let package = GetPackage(openPackage, path, nugetRequest) + where lookupPackageName.Equals(new PackageName(package.Id, package.Version)) + select package).FirstOrDefault(); + } + + /// + /// Find the package files (.nupkg or .nuspec). + /// + /// + /// + /// + private IEnumerable GetPackageLookupPaths(string packageId, SemanticVersion version) { + // Files created by the path resolver. This would take into account the non-side-by-side scenario + // and we do not need to match this for id and version. + //var packageFileName = packageId + version + Constant.PackageExtension; + + var packageFileName = FileUtility.MakePackageFileName(false, packageId, version.ToString(), NuGetConstant.PackageExtension); + + // var packageFileName = PathResolver.GetPackageFileName(packageId, version); + var manifestFileName = Path.ChangeExtension(packageFileName, NuGetConstant.ManifestExtension); + + var filesMatchingFullName = GetPackageFiles(packageFileName).Concat(GetPackageFiles(manifestFileName)); + + if (version.Version.Revision < 1) { + // If the build or revision number is not set, we need to look for combinations of the format + // * Foo.1.2.nupkg + // * Foo.1.2.3.nupkg + // * Foo.1.2.0.nupkg + // * Foo.1.2.0.0.nupkg + // To achieve this, we would look for files named 1.2*.nupkg if both build and revision are 0 and + // 1.2.3*.nupkg if only the revision is set to 0. + string partialName = version.Version.Build < 1 ? + String.Join(".", packageId, version.Version.Major, version.Version.Minor) : + String.Join(".", packageId, version.Version.Major, version.Version.Minor, version.Version.Build); + + string partialManifestName = partialName + "*" + NuGetConstant.ManifestExtension; + + partialName += "*" + NuGetConstant.PackageExtension; + + // Partial names would result is gathering package with matching major and minor but different build and revision. + // Attempt to match the version in the path to the version we're interested in. + var partialNameMatches = GetPackageFiles(partialName).Where(path => FileNameMatchesPattern(packageId, version, path)); + + var partialManifestNameMatches = GetPackageFiles(partialManifestName).Where( + path => FileNameMatchesPattern(packageId, version, path)); + + return filesMatchingFullName.Concat(partialNameMatches).Concat(partialManifestNameMatches); + } + + return filesMatchingFullName; + } + + /// + /// True if the the file contains the right id and version. + /// + /// package id + /// package version + /// File path + /// + private static bool FileNameMatchesPattern(string packageId, SemanticVersion version, string path) + { + var name = Path.GetFileNameWithoutExtension(path); + SemanticVersion parsedVersion; + + if (string.IsNullOrWhiteSpace(name)) + { + return false; + } + + // When matching by pattern, we will always have a version token. Packages without versions would be matched early on by the version-less path resolver + // when doing an exact match. + return name.Length > packageId.Length && + SemanticVersion.TryParse(name.Substring(packageId.Length + 1), out parsedVersion) && + parsedVersion == version; + } + + /// + /// Find-Package based the given Id + /// + /// Package Id + /// + /// + public IEnumerable FindPackagesById(string packageId, NuGetRequest request) + { + return FindPackagesById(OpenPackage, packageId, request); + } + + /// + /// Find-Package based the given Id + /// + /// Delegate function which is actually finding a package + /// Package version + /// + /// + private IEnumerable FindPackagesById(Func openPackage, string packageId, Request request) + { + request.Debug(Resources.Messages.DebugInfoCallMethod3, "LocalPackageRepository", "FindPackagesById", packageId); + + // get packages in .nupkg or .nuspec files + // add "*" to avoid parsing the version (id: packageName+Version, e.g. jQuery.1.10.0) + return GetPackages( + openPackage, + packageId, + GetPackageFiles(packageId + "*" + NuGetConstant.PackageExtension), + request).Union( + GetPackages( + openPackage, + packageId, + GetPackageFiles(packageId + "*" + NuGetConstant.ManifestExtension), + request)); + + } + + /// + /// True if the packagesource is a file repository + /// + public bool IsFile { + get { + //true because this is not a local file repository + return true; + } + } + + /// + /// Search the entire repository for the case when a user does not provider package name or uses wildcards in the name. + /// + /// The Searchterm + /// + /// + public IEnumerable Search(string searchTerm, NuGetRequest nugetRequest) + { + var packages = SearchImpl(searchTerm, nugetRequest); + if (packages == null) { + return Enumerable.Empty(); + } + + if (nugetRequest != null && nugetRequest.AllVersions.Value) { + //return whatever we can find + return packages; + } + + //return the lastest version + return packages.GroupBy(p => p.Id).Select(each => each.OrderByDescending(pp => pp.Version).FirstOrDefault()); + } + + private IEnumerable SearchImpl(string searchTerm, NuGetRequest nugetRequest) + { + if (nugetRequest == null) { + yield break; + } + + nugetRequest.Debug(Resources.Messages.SearchingRepository, "LocalPackageRepository", Source); + + var files = Directory.GetFiles(Source); + + foreach (var package in nugetRequest.FilterOnTags(files.Select(nugetRequest.GetPackageByFilePath).Where(pkgItem => pkgItem != null).Select(pkg => pkg.Package))) + { + yield return package; + } + + // look in the package source location for directories that contain nupkg files. + var subdirs = Directory.EnumerateDirectories(Source, "*", SearchOption.AllDirectories); + foreach (var subdir in subdirs) { + var nupkgs = Directory.EnumerateFileSystemEntries(subdir, "*.nupkg", SearchOption.TopDirectoryOnly); + + foreach (var package in nugetRequest.FilterOnTags(nupkgs.Select(nugetRequest.GetPackageByFilePath).Where(pkgItem => pkgItem != null).Select(pkg => pkg.Package))) + { + yield return package; + } + } + } + } +} diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Repository/PackageRepositoryFactory.cs b/src/Microsoft.PackageManagement.NuGetProvider/Repository/PackageRepositoryFactory.cs new file mode 100644 index 000000000..771651e32 --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Repository/PackageRepositoryFactory.cs @@ -0,0 +1,32 @@ +namespace Microsoft.PackageManagement.NuGetProvider +{ + using System; + using System.Net; + + public class PackageRepositoryFactory : IPackageRepositoryFactory + { + private static readonly PackageRepositoryFactory _default = new PackageRepositoryFactory(); + + public static PackageRepositoryFactory Default + { + get { return _default;} + } + + public virtual IPackageRepository CreateRepository(string packageSource, NuGetRequest request) + { + if (packageSource == null) + { + throw new ArgumentNullException("packageSource"); + } + + Uri uri = new Uri(packageSource); + + if (uri.IsFile) + { + return new LocalPackageRepository(uri.LocalPath, request); + } + + return new HttpClientPackageRepository(packageSource, request); + } + } +} diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Sdk/Constants.cs b/src/Microsoft.PackageManagement.NuGetProvider/Sdk/Constants.cs new file mode 100644 index 000000000..1d1268a61 --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Sdk/Constants.cs @@ -0,0 +1,109 @@ + +namespace Microsoft.PackageManagement.NuGetProvider +{ + internal static class Constants + { + #region copy common-constants-implementation /internal/public + + internal const string MinVersion = "0.0.0.1"; + internal const string MSGPrefix = "MSG:"; + internal const string Download = "Download"; + internal const string Install = "Install"; + public readonly static string[] FeaturePresent = new string[0]; + internal const string CurrentUser = "CurrentUser"; + internal const string AllUsers = "AllUsers"; + + //public static string[] Empty = new string[0]; + + internal static class Features + { + public const string AutomationOnly = "automation-only"; + public const string MagicSignatures = "magic-signatures"; + public const string SupportedExtensions = "file-extensions"; + public const string SupportedSchemes = "uri-schemes"; + public const string SupportsPowerShellModules = "supports-powershell-modules"; + public const string SupportsRegexSearch = "supports-regex-search"; + public const string SupportsSubstringSearch = "supports-substring-search"; + public const string SupportsWildcardSearch = "supports-wildcard-search"; + } + + internal static class Messages + { + public const string UnableToFindDependencyPackage = "MSG:UnableToFindDependencyPackage"; + public const string DependentPackageFailedInstallOrDownload = "MSG:DependentPackageFailedInstallOrDownload"; + public const string MissingRequiredParameter = "MSG:MissingRequiredParameter"; + public const string PackageFailedInstallOrDownload = "MSG:PackageFailedInstallOrDownload"; + public const string PackageSourceExists = "MSG:PackageSourceExists"; + public const string ProtocolNotSupported = "MSG:ProtocolNotSupported"; + public const string ProviderPluginLoadFailure = "MSG:ProviderPluginLoadFailure"; + public const string ProviderSwidtagUnavailable = "MSG:ProviderSwidtagUnavailable"; + public const string RemoveEnvironmentVariableRequiresElevation = "MSG:RemoveEnvironmentVariableRequiresElevation"; + public const string SchemeNotSupported = "MSG:SchemeNotSupported"; + public const string SourceLocationNotValid = "MSG:SourceLocationNotValid"; + public const string UnableToCopyFileTo = "MSG:UnableToCopyFileTo"; + public const string UnableToCreateShortcutTargetDoesNotExist = "MSG:UnableToCreateShortcutTargetDoesNotExist"; + public const string UnableToDownload = "MSG:UnableToDownload"; + public const string UnableToOverwriteExistingFile = "MSG:UnableToOverwriteExistingFile"; + public const string UnableToRemoveFile = "MSG:UnableToRemoveFile"; + public const string UnableToResolvePackage = "MSG:UnableToResolvePackage"; + public const string UnableToResolveSource = "MSG:UnableToResolveSource"; + public const string UnableToUninstallPackage = "MSG:UnableToUninstallPackage"; + public const string UnknownFolderId = "MSG:UnknownFolderId"; + public const string UnknownProvider = "MSG:UnknownProvider"; + public const string UnsupportedArchive = "MSG:UnsupportedArchive"; + public const string UnsupportedProviderType = "MSG:UnsupportedProviderType"; + public const string UriSchemeNotSupported = "MSG:UriSchemeNotSupported"; + public const string UserDeclinedUntrustedPackageInstall = "MSG:UserDeclinedUntrustedPackageInstall"; + public const string HashNotFound = "MSG:HashNotFound"; + public const string HashNotMatch = "MSG:HashNotMatch"; + public const string HashNotSupported = "MSG:HashNotSupported"; + public const string DependencyLoopDetected = "MSG:DependencyLoopDetected"; + public const string CouldNotGetResponseFromQuery = "MSG:CouldNotGetResponseFromQuery"; + public const string SkippedDownloadedPackage = "MSG:SkippedDownloadedPackage"; + public const string InstallRequiresCurrentUserScopeParameterForNonAdminUser = "MSG:InstallRequiresCurrentUserScopeParameterForNonAdminUser"; + + } + + internal static class OptionType { + public const string String = "String"; + public const string StringArray = "StringArray"; + public const string Int = "Int"; + public const string Switch = "Switch"; + public const string Folder = "Folder"; + public const string File = "File"; + public const string Path = "Path"; + public const string Uri = "Uri"; + public const string SecureString = "SecureString"; + } + + internal static class PackageStatus + { + public const string Available = "Available"; + public const string Dependency = "Dependency"; + public const string Installed = "Installed"; + public const string Uninstalled = "Uninstalled"; + } + + internal static class Parameters + { + public const string IsUpdate = "IsUpdatePackageSource"; + public const string Name = "Name"; + public const string Location = "Location"; + } + + internal static class Signatures + { + public const string Cab = "4D534346"; + public const string OleCompoundDocument = "D0CF11E0A1B11AE1"; + public const string Zip = "504b0304"; + //public static string[] ZipVariants = new[] {Zip, /* should have EXEs? */}; + } + + internal static class SwidTag + { + public const string SoftwareIdentity = "SoftwareIdentity"; + } + + #endregion + } +} diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Sdk/ErrorCategory.cs b/src/Microsoft.PackageManagement.NuGetProvider/Sdk/ErrorCategory.cs new file mode 100644 index 000000000..992032d1d --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Sdk/ErrorCategory.cs @@ -0,0 +1,38 @@ + +namespace Microsoft.PackageManagement.NuGetProvider +{ + public enum ErrorCategory { + NotSpecified, + OpenError, + CloseError, + DeviceError, + DeadlockDetected, + InvalidArgument, + InvalidData, + InvalidOperation, + InvalidResult, + InvalidType, + MetadataError, + NotImplemented, + NotInstalled, + ObjectNotFound, + OperationStopped, + OperationTimeout, + SyntaxError, + ParserError, + PermissionDenied, + ResourceBusy, + ResourceExists, + ResourceUnavailable, + ReadError, + WriteError, + FromStdErr, + SecurityError, + ProtocolError, + ConnectionError, + AuthenticationError, + LimitsExceeded, + QuotaExceeded, + NotEnabled, + } +} diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Sdk/Request.cs b/src/Microsoft.PackageManagement.NuGetProvider/Sdk/Request.cs new file mode 100644 index 000000000..13bc8bbb8 --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Sdk/Request.cs @@ -0,0 +1,338 @@ +namespace Microsoft.PackageManagement.NuGetProvider +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.Linq; + using System.Security; + using Resources; + using System.Net; + + public abstract class Request { + //private Dictionary _options; + private string[] _packageSources; + + #region PackageMangaement Interfaces + + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "It's a generated code")] + public interface IProviderServices { + bool IsElevated { get; } + + IEnumerable FindPackageByCanonicalId(string canonicalId, Request requestObject); + + string GetCanonicalPackageId(string providerName, string packageName, string version, string source); + + string ParseProviderName(string canonicalPackageId); + + string ParsePackageName(string canonicalPackageId); + + string ParsePackageVersion(string canonicalPackageId); + + string ParsePackageSource(string canonicalPackageId); + + void DownloadFile(Uri remoteLocation, string localFilename, Request requestObject); + + bool IsSupportedArchive(string localFilename, Request requestObject); + + IEnumerable UnpackArchive(string localFilename, string destinationFolder, Request requestObject); + + bool Install(string fileName, string additionalArgs, Request requestObject); + + bool IsSignedAndTrusted(string filename, Request requestObject); + } + + [SuppressMessage("Microsoft.Design", "CA1040:AvoidEmptyInterfaces", Justification = "It's a generated code")] + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "It's a generated code")] + public interface IPackageProvider { + + } + + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "It's a generated code")] + public interface IPackageManagementService { + int Version { get; } + + IEnumerable ProviderNames { get; } + + IEnumerable AllProviderNames { get; } + + IEnumerable PackageProviders { get; } + + IEnumerable SelectProvidersWithFeature(string featureName); + + IEnumerable SelectProvidersWithFeature(string featureName, string value); + + IEnumerable SelectProviders(string providerName, Request requestObject); + + bool RequirePackageProvider(string requestor, string packageProviderName, string minimumVersion, Request requestObject); + } + #endregion + + #region core-apis + + public abstract dynamic PackageManagementService {get;} + + public abstract IProviderServices ProviderServices {get;} + + #endregion + + #region copy host-apis + + /* Synced/Generated code =================================================== */ + public abstract bool IsCanceled {get;} + + public abstract string GetMessageString(string messageText, string defaultText); + + public abstract bool Warning(string messageText); + + [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", Justification = "It's a generated code")] + public abstract bool Error(string id, string category, string targetObjectValue, string messageText); + + public abstract bool Message(string messageText); + + public abstract bool Verbose(string messageText); + + public abstract bool Debug(string messageText); + + public abstract int StartProgress(int parentActivityId, string messageText); + + public abstract bool Progress(int activityId, int progressPercentage, string messageText); + + public abstract bool CompleteProgress(int activityId, bool isSuccessful); + + /// + /// Used by a provider to request what metadata keys were passed from the user + /// + /// + public abstract IEnumerable OptionKeys {get;} + + /// + /// + /// + /// + public abstract IEnumerable GetOptionValues(string key); + + public abstract IEnumerable Sources {get;} + + public abstract string CredentialUsername {get;} + + public abstract SecureString CredentialPassword {get;} + + public abstract IWebProxy WebProxy { get; } + + public abstract bool ShouldBootstrapProvider(string requestor, string providerName, string providerVersion, string providerType, string location, string destination); + + public abstract bool ShouldContinueWithUntrustedPackageSource(string package, string packageSource); + + public abstract bool ShouldContinue(string query, string caption, ref bool yesToAll, ref bool noToAll); + + public abstract bool ShouldContinue(string query, string caption); + + public abstract bool AskPermission(string permission); + + public abstract bool IsInteractive {get;} + + public abstract int CallCount {get;} + + #endregion + + #region copy response-apis + + /* Synced/Generated code =================================================== */ + + /// + /// Used by a provider to return fields for a SoftwareIdentity. + /// + /// + /// + /// + /// IPackageProvider + /// + /// + /// + /// + /// + /// + public abstract string YieldSoftwareIdentity(string fastPath, string name, string version, string versionScheme, string summary, string source, string searchKey, string fullPath, string packageFileName); + + public abstract string AddMetadata(string name, string value); + + public abstract string AddMetadata(string elementPath, string name, string value); + + [SuppressMessage("Microsoft.Naming", "CA1716:IdentifiersShouldNotMatchKeywords", Justification = "It's a generated code")] + public abstract string AddMetadata(string elementPath, Uri @namespace, string name, string value); + + public abstract string AddTagId(string tagId); + + public abstract string AddCulture(string xmlLang); + + public abstract string AddMeta(string elementPath); + + public abstract string AddEntity(string name, string regid, string role, string thumbprint); + + public abstract string AddLink(Uri referenceUri, string relationship, string mediaType, string ownership, string use, string appliesToMedia, string artifact); + + public abstract string AddDependency(string providerName, string packageName, string version, string source, string appliesTo); + + public abstract string AddPayload(); + + public abstract string AddEvidence(DateTime dateTime, string deviceId); + + public abstract string AddDirectory(string elementPath, string directoryName, string location, string root, bool isKey); + + public abstract string AddFile(string elementPath, string fileName, string location, string root, bool isKey, long size, string version); + + public abstract string AddProcess(string elementPath, string processName, int pid); + + public abstract string AddResource(string elementPath, string type); + + /// + /// Used by a provider to return fields for a package source (repository) + /// + /// + /// + /// + /// + /// + /// + public abstract bool YieldPackageSource(string name, string location, bool isTrusted, bool isRegistered, bool isValidated); + + /// + /// Used by a provider to return the fields for a Metadata Definition + /// The cmdlets can use this to supply tab-completion for metadata to the user. + /// + /// the provider-defined name of the option + /// one of ['string','int','path','switch'] + /// if the parameter is mandatory + /// + public abstract bool YieldDynamicOption(string name, string expectedType, bool isRequired); + + public bool YieldDynamicOption(string name, string expectedType, bool isRequired, IEnumerable permittedValues) + { + return YieldDynamicOption(name, expectedType, isRequired) && (permittedValues ?? Enumerable.Empty()).All(each => YieldKeyValuePair(name, each)); + } + + public abstract bool YieldKeyValuePair(string key, string value); + + public abstract bool YieldValue(string value); + + #endregion + /// + /// Yield values in a dictionary as key/value pairs. (one pair for each value in each key) + /// + /// + /// + internal bool Yield(Dictionary dictionary) { + + + return dictionary.All(Yield); + } + + internal bool Yield(KeyValuePair pair) + { + if (pair.Value.Length == 0) { + return YieldKeyValuePair(pair.Key, null); + } + return pair.Value.All(each => YieldKeyValuePair(pair.Key, each)); + } + + internal bool WriteError(ErrorCategory category, string targetObjectValue, string messageText, params object[] args) + { + return Error(messageText, category.ToString(), targetObjectValue, FormatMessageString(messageText, args)); + } + + internal bool Warning(string messageText, params object[] args) + { + return Warning(FormatMessageString(messageText, args)); + } + + //internal bool Message(string messageText, params object[] args) + //{ + // return Message(FormatMessageString(messageText, args)); + //} + + internal bool Verbose(string messageText, params object[] args) + { + return Verbose(FormatMessageString(messageText, args)); + } + + internal bool Debug(string messageText, params object[] args) + { + return Debug(FormatMessageString(messageText, args)); + } + + //internal int StartProgress(int parentActivityId, string messageText, params object[] args) + //{ + // return StartProgress(parentActivityId, FormatMessageString(messageText, args)); + //} + + //internal bool Progress(int activityId, int progressPercentage, string messageText, params object[] args) { + // return Progress(activityId, progressPercentage, FormatMessageString(messageText, args)); + //} + + internal string GetOptionValue(string name) + { + // get the value from the request + return (GetOptionValues(name) ?? Enumerable.Empty()).LastOrDefault(); + } + + private static string FixMeFormat(string formatString, object[] args) { + if (args == null || args.Length == 0) { + // not really any args, and not really expectng any + return formatString.Replace('{', '\u00ab').Replace('}', '\u00bb'); + } + return args.Aggregate(formatString.Replace('{', '\u00ab').Replace('}', '\u00bb'), (current, arg) => current + string.Format(CultureInfo.CurrentCulture, " \u00ab{0}\u00bb", arg)); + } + + internal string GetMessageStringInternal(string messageText) { + return Messages.ResourceManager.GetString(messageText); + } + + internal string FormatMessageString(string messageText, params object[] args) { + if (string.IsNullOrWhiteSpace(messageText)) + { + return string.Empty; + } + + if (args == null) { + return messageText; + } + + if (messageText.Contains(Constants.MSGPrefix)) { + // if (messageText.StartsWith(Constants.MSGPrefix, true, CultureInfo.CurrentCulture)) { + // check with the caller first, then with the local resources, and fallback to using the messageText itself. + messageText = GetMessageString(messageText.Substring(Constants.MSGPrefix.Length), GetMessageStringInternal(messageText) ?? messageText) + // checks whether we have the message text in our resource table (have to take out the msg prefix) + ?? GetMessageStringInternal(messageText.Substring(Constants.MSGPrefix.Length)) + ?? messageText; + } + + // if it doesn't look like we have the correct number of parameters + // let's return a fix-me-format string. + var c = messageText.ToCharArray().Where(each => each == '{').Count(); + if (c < args.Length) { + return FixMeFormat(messageText, args); + } + return string.Format(CultureInfo.CurrentCulture, messageText, args); + } + + //internal bool YieldDynamicOption(string name, string expectedType, bool isRequired, IEnumerable permittedValues) + //{ + // return YieldDynamicOption(name, expectedType, isRequired) && (permittedValues ?? Enumerable.Empty()).All(each => YieldKeyValuePair(name, each)); + //} + + //internal Dictionary Options + //{ + // get { + // return _options ?? (_options = OptionKeys.Where(each => !string.IsNullOrWhiteSpace(each)).ToDictionary(k => k, (k) => (GetOptionValues(k) ?? new string[0]).ToArray())); + // } + //} + + internal IEnumerable PackageSources + { + get { + return _packageSources ?? (_packageSources = (Sources ?? new string[0]).ToArray()); + } + } + } +} diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Utility/NuGetPathUtility.cs b/src/Microsoft.PackageManagement.NuGetProvider/Utility/NuGetPathUtility.cs new file mode 100644 index 000000000..6a8559eec --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Utility/NuGetPathUtility.cs @@ -0,0 +1,218 @@ + +using Microsoft.PackageManagement.Provider.Utility; + +namespace Microsoft.PackageManagement.NuGetProvider +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.IO; + using System.Linq; + using System.Net; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using System.Runtime.InteropServices; + + internal static class NuGetPathUtility + { + private static readonly char[] _invalidPathChars = Path.GetInvalidPathChars(); + + + internal static bool IsManifest(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return false; + } + + return Path.GetExtension(path).Equals(NuGetConstant.ManifestExtension, StringComparison.OrdinalIgnoreCase); + } + + internal static bool IsPackageFile(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return false; + } + + return Path.GetExtension(path).Equals(NuGetConstant.PackageExtension, StringComparison.OrdinalIgnoreCase); + } + + internal static bool ValidateSourceUri(IEnumerable supportedSchemes, Uri srcUri, NuGetRequest request) + { + + if (!supportedSchemes.Contains(srcUri.Scheme.ToLowerInvariant())) + { + return false; + } + + if (srcUri.IsFile) + { + //validate file source location + if (Directory.Exists(srcUri.LocalPath)) + { + return true; + } + return false; + } + + //validate uri source location + return ValidateUri(srcUri, request) != null; + } + + /// + /// Returns the validated uri. Returns null if we cannot validate it + /// + /// + /// + /// + internal static Uri ValidateUri(Uri query, NuGetRequest request) + { + var client = request.ClientWithoutAcceptHeader; + + var response = PathUtility.GetHttpResponse(client, query.AbsoluteUri, (() => request.IsCanceled), + ((msg, num) => request.Verbose(Resources.Messages.RetryingDownload, msg, num)), (msg) => request.Verbose(msg), (msg) => request.Debug(msg)); + + if (response == null) + { + return null; + } + + // if response is not success, we need to check for redirection + if (!response.IsSuccessStatusCode) + { + // Check for redirection (http status code 3xx) + if (response.StatusCode == HttpStatusCode.MultipleChoices || response.StatusCode == HttpStatusCode.MovedPermanently + || response.StatusCode == HttpStatusCode.Found || response.StatusCode == HttpStatusCode.SeeOther + || response.StatusCode == HttpStatusCode.TemporaryRedirect) + { + // get the redirected direction + string location = response.Headers.GetValues("Location").FirstOrDefault(); + if (String.IsNullOrWhiteSpace(location)) + { + return null; + } + + // make a new query based on location + query = new Uri(location); + } + else + { + // other status code is wrong + return null; + } + } + else + { + query = new Uri(response.RequestMessage.RequestUri.AbsoluteUri); + } + + //Making a query like: www.nuget.org/api/v2/FindPackagesById()?id='FoooBarr' to check the server available. + //'FoooBarr' is an any random package id + string queryUri = "FoooBarr".MakeFindPackageByIdQuery(PathUtility.UriCombine(query.AbsoluteUri, NuGetConstant.FindPackagesById)); + + response = PathUtility.GetHttpResponse(client, queryUri, (() => request.IsCanceled), + ((msg, num) => request.Verbose(Resources.Messages.RetryingDownload, msg, num)), (msg) => request.Verbose(msg), (msg) => request.Debug(msg)); + + + // The link is not valid + if (response == null || !response.IsSuccessStatusCode) + { + return null; + } + + return query; + } + +#region CryptProtectData + //internal struct DATA_BLOB + //{ + // public int cbData; + // public IntPtr pbData; + //} + + //internal static void CopyByteToBlob(ref DATA_BLOB blob, byte[] data) + //{ + // blob.pbData = Marshal.AllocHGlobal(data.Length); + + // blob.cbData = data.Length; + + // Marshal.Copy(data, 0, blob.pbData, data.Length); + //} + + //internal const uint CRYPTPROTECT_UI_FORBIDDEN = 0x1; + + //[DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + //private static extern bool CryptUnprotectData(ref DATA_BLOB pDataIn, ref string ppszDataDescr, ref DATA_BLOB pOptionalEntropy, + // IntPtr pvReserved, IntPtr pPromptStruct, uint dwFlags, ref DATA_BLOB pDataOut); + + //[DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + //private static extern bool CryptProtectData(ref DATA_BLOB pDataIn, string szDataDescr, ref DATA_BLOB pOptionalEntropy, + // IntPtr pvReserved, IntPtr pPromptStruct, uint dwFlags, ref DATA_BLOB pDataOut); + + //public static byte[] CryptProtect(byte[] dataIn, byte[] optionalEntropy, bool encryptionOperation) + //{ + // DATA_BLOB dataInBlob = new DATA_BLOB(); + // DATA_BLOB optionalEntropyBlob = new DATA_BLOB(); + // DATA_BLOB resultBlob = new DATA_BLOB(); + // string description = String.Empty; + + // try + // { + // // copy the encrypted blob + // CopyByteToBlob(ref dataInBlob, dataIn); + // CopyByteToBlob(ref optionalEntropyBlob, optionalEntropy); + + // // use local user + // uint flags = CRYPTPROTECT_UI_FORBIDDEN; + + // bool success = false; + + // // doing decryption + // if (!encryptionOperation) + // { + // // call win32 api + // success = CryptUnprotectData(ref dataInBlob, ref description, ref optionalEntropyBlob, IntPtr.Zero, IntPtr.Zero, flags, ref resultBlob); + // } + // else + // { + // // doing encryption + // success = CryptProtectData(ref dataInBlob, description, ref optionalEntropyBlob, IntPtr.Zero, IntPtr.Zero, flags, ref resultBlob); + // } + + // if (!success) + // { + // throw new Win32Exception(Marshal.GetLastWin32Error()); + // } + + // byte[] unencryptedBytes = new byte[resultBlob.cbData]; + + // Marshal.Copy(resultBlob.pbData, unencryptedBytes, 0, resultBlob.cbData); + + // return unencryptedBytes; + // } + // finally + // { + // // free memory + // if (dataInBlob.pbData != IntPtr.Zero) + // { + // Marshal.FreeHGlobal(dataInBlob.pbData); + // } + + // if (optionalEntropyBlob.pbData != IntPtr.Zero) + // { + // Marshal.FreeHGlobal(optionalEntropyBlob.pbData); + // } + + // if (resultBlob.pbData != IntPtr.Zero) + // { + // Marshal.FreeHGlobal(resultBlob.pbData); + // } + // } + //} +#endregion + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Utility/PackageUtility.cs b/src/Microsoft.PackageManagement.NuGetProvider/Utility/PackageUtility.cs new file mode 100644 index 000000000..e45ba24af --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Utility/PackageUtility.cs @@ -0,0 +1,688 @@ +namespace Microsoft.PackageManagement.NuGetProvider +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.IO.Compression; + using System.Linq; + using System.Text.RegularExpressions; + using System.Xml; + using System.Xml.Linq; + using Resources; + using Microsoft.PackageManagement.Provider.Utility; + + /// + /// Utility that can be used to handle the packaged related actions. + /// + internal static class PackageUtility + { + + // the pre release regex is of the form - where version is set of identifier delimited by ".". Each identifer can be any characters in [A-z0-9a-z-] + // The build regex is of the same form except with a + instead of - + private static string VersionPattern = String.Concat(@"\.", SemanticVersion.AllowFourPartsVersion, SemanticVersion.ReleasePattern, SemanticVersion.BuildPattern, "$"); + + /// + /// Get the package name without version info + /// + /// + /// + internal static string GetPackageNameWithoutVersionInfo(string packageName) + { + //Input: JQuery.2.1.3 + //output: Jqery + // An unsupported scenario is if the package has name MyModule2.2 and version 2.0 then we will return version as 2.2.2.0 and packagename as mymodule + + string version = String.Empty; + + // Check for version + var m1 = Regex.Match(packageName, VersionPattern, RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + if (m1.Success) { + // We have a version + version = m1.ToString(); + } + + // Return name without the version + return packageName.Substring(0, packageName.Length - version.Length); + } + + + internal static IEnumerable ProcessPackagesFeed(Stream stream) + { + if (stream == null) { + throw new ArgumentNullException("stream"); + } + + XDocument document = XmlUtility.LoadSafe(stream, ignoreWhiteSpace: true); + + var entries = document.Root.ElementsNoNamespace("entry"); + + if (entries == null) + { + + var message = string.Format(Messages.ManifestRequiredXmlElementMissing, "entry"); + throw new InvalidDataException(message); + } + + foreach (XElement entry in entries) + { + var package = new PackageBase(); + + ReadEntryElement(ref package, entry); + + yield return package; + } + } + + /// + /// Parse the 'entry' xml element. + /// + /// + /// + internal static void ReadEntryElement(ref PackageBase package, XElement xElement) + { + var node = xElement.FirstNode; + + //iterate each element from the feed, e.g. + //http://www.nuget.org/api/v2/FindPackagesById()?id='Jquery' + // + while (node != null) + { + var element = node as XElement; + + if (element != null) + { + ReadEntryChildNode(ref package, element); + } + node = node.NextNode; + } + } + + /// + /// Parse the children of the 'entry' tag. + /// + /// + /// + private static void ReadEntryChildNode(ref PackageBase package, XElement element) + { + + var value = element.Value.SafeTrim(); + + switch (element.Name.LocalName.ToLowerInvariant()) + { + case "id": + //In manifest, http://www.nuget.org/api/v2/Packages(Id='jQuery',Version='1.10.1') + //we here only need Id=Jquery + package.Id = ExtractId(value); + break; + case "version": + package.Version = value; + break; + case "minclientversion": + package.MinClientVersion = string.IsNullOrWhiteSpace(value) ? null : new Version(value); + break; + case "author": + package.Authors = value; + break; + case "owners": + package.Owners = value; + break; + case "licenseurl": + package.LicenseUrl = string.IsNullOrWhiteSpace(value) ? null : new Uri(value); + break; + case "projecturl": + package.ProjectUrl = string.IsNullOrWhiteSpace(value) ? null : new Uri(value); + break; + case "iconurl": + package.IconUrl = string.IsNullOrWhiteSpace(value) ? null : new Uri(value); + break; + case "gallerydetailsurl": + package.GalleryDetailsUrl = string.IsNullOrWhiteSpace(value) ? null : new Uri(value); + break; + case "requirelicenseacceptance": + package.RequireLicenseAcceptance = XmlConvert.ToBoolean(value); + break; + case "developmentdependency": + package.DevelopmentDependency = XmlConvert.ToBoolean(value); + break; + case "description": + package.Description = value; + break; + case "summary": + package.Summary = value; + break; + case "content": + var srcAttribute = element.Attributes(XName.Get("src")).FirstOrDefault(); + if (srcAttribute != null) { + package.ContentSrcUrl = srcAttribute.Value; + } + break; + case "releasenotes": + package.ReleaseNotes = value; + break; + case "copyright": + package.Copyright = value; + break; + case "language": + package.Language = value; + break; + case "title": + package.Title = value; + break; + case "tags": + package.Tags = value; + break; + case "islatestversion": + package.IsLatestVersion = Convert.ToBoolean(value, CultureInfo.InvariantCulture); + break; + case "isabsolutelatestversion": + package.IsAbsoluteLatestVersion = Convert.ToBoolean(value, CultureInfo.InvariantCulture); + break; + case "published": + package.Published = GetDateTime(value); + break; + case "created": + package.Created = GetDateTime(value); + break; + case "lastupdated": + package.LastUpdated = GetDateTime(value); + break; + case "lastedited": + package.LastEdited = GetDateTime(value); + break; + case "licensereporturl": + package.LicenseReportUrl = string.IsNullOrWhiteSpace(value) ? null : new Uri(value); + break; + case "reportabuseurl": + package.ReportAbuseUrl = string.IsNullOrWhiteSpace(value) ? null : new Uri(value); + break; + case "downloadcount": + package.DownloadCount = XmlConvert.ToInt64(value); + break; + case "versiondownloadcount": + package.VersionDownloadCount = XmlConvert.ToInt64(value); + break; + case "packagesize": + package.PackageSize = XmlConvert.ToInt64(value); + break; + case "properties": + var node = element.FirstNode; + while (node != null) + { + var propertyElement = node as XElement; + if (propertyElement != null) + { + ReadEntryChildNode(ref package, propertyElement); + } + node = node.NextNode; + } + break; + case "dependencies": + package.DependencySetList = ParseDependencySet(value); + break; + case "packagehashalgorithm": + package.PackageHashAlgorithm = value; + break; + case "packagehash": + package.PackageHash = value; + break; + /* case "frameworkAssemblies": + package.FrameworkAssemblies = ReadFrameworkAssemblies(element); + break; + case "references": + package.ReferenceSets = ReadReferenceSets(element); + break;*/ + default: + if (!String.IsNullOrWhiteSpace(value) && !String.IsNullOrWhiteSpace(element.Name.LocalName)) + { + package.AdditionalProperties.AddOrSet(element.Name.LocalName, value); + } + break; + } + } + + internal static DateTimeOffset? GetDateTime(string dateTime) + { + if (string.IsNullOrWhiteSpace(dateTime)) + { + return null as DateTimeOffset?; + } + + DateTime result; + + if (DateTime.TryParse(dateTime, out result)) + { + return result; + } + else + { + return null as DateTimeOffset?; + } + } + + /// + /// Parse the dependency set from string value (returned from nuget api call) + /// + /// + /// + private static List ParseDependencySet(string value) + { + var dependencySets = new List(); + + if (string.IsNullOrWhiteSpace(value)) + { + return dependencySets; + } + + // Dependencies are of the form "dep1|dep2" + // Split them up and process each depedency with ParseDependency function + var dependencies = value.Split('|').Select(ParseDependency); + + // group the dependencies by target framework + var groups = dependencies.GroupBy(d => d.Item3); + + dependencySets.AddRange( + // Construct dependencySets by targetframework + groups.Select(g => new PackageDependencySet + { + TargetFramework = g.Key, + // Construct the DependencyList for this framework + Dependencies = g.Where(pair => !String.IsNullOrEmpty(pair.Item1)) // the Id is empty when a group is empty. + .Select(pair => new PackageDependency + { + Id = pair.Item1, + DependencyVersion = pair.Item2 + }).ToList() + })); + return dependencySets; + } + + /// + /// Parses a dependency from the feed in the format: + /// id or id:versionSpec, or id:versionSpec:targetFramework + /// + private static Tuple ParseDependency(string value) + { + if (String.IsNullOrWhiteSpace(value)) + { + return null; + } + + // IMPORTANT: Do not pass StringSplitOptions.RemoveEmptyEntries to this method, because it will break + // if the version spec is null, for in that case, the Dependencies string sent down is "::". + // We do want to preserve the second empty element after the split. + string[] tokens = value.Trim().Split(new[] { ':' }); + + if (tokens.Length == 0) + { + return null; + } + + // Trim the id + string id = tokens[0].Trim(); + + // Parse the dependency version + DependencyVersion depVer = new DependencyVersion(); + if (tokens.Length > 1) + { + // Parse the version + depVer = DependencyVersion.ParseDependencyVersion(tokens[1]); + } + + // Get the target framework, returns empty string if none exists. + var targetFramework = (tokens.Length > 2 && !String.IsNullOrEmpty(tokens[2])) ? tokens[2] : String.Empty; + + return Tuple.Create(id, depVer, targetFramework); + } + + /// + /// Extract the package id. + /// From the feed, the package id is eqaul to something like this "http://www.nuget.org/api/v2/Packages(Id='jQuery',Version='2.1.3')". + /// But we need id='jQuery'. + /// + /// + /// + private static string ExtractId(string longId) + { + String pattern = @"([^']+?)\+?'(?[^']+?)\'"; + + Match m = Regex.Match(longId, pattern, RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + + if (m.Success) + { + return m.Groups["PackageId"].ToString(); + } + + return longId; + } + + /// + /// Utility to parse the nuspec file and produces the package object. + /// + /// The full path to the .nuspec file + /// + internal static PackageBase ProcessNuspec(string nuspecFileFullPath) + { + + if (string.IsNullOrWhiteSpace(nuspecFileFullPath)) + { + throw new ArgumentNullException(nuspecFileFullPath); + } + + if (!File.Exists(nuspecFileFullPath)) + { + throw new FileNotFoundException(nuspecFileFullPath); + } + + //Make sure it's .nuspec + string fileExtension = Path.GetExtension(nuspecFileFullPath); + + if (fileExtension != ".nuspec") + { + throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, Messages.InvalidFileExtension, nuspecFileFullPath)); + } + + XDocument document = XmlUtility.LoadSafe(nuspecFileFullPath); + return ProcessNuspec(document); + } + + + /// + /// Utility to parse the nuspec file and produces the package object. + /// + /// XDocument object pointing to a .nuspec xml file + /// + private static PackageBase ProcessNuspec(XDocument rootDocument) + { + if (rootDocument == null) + { + throw new ArgumentNullException("rootDocument"); + } + + var pkg = new PackageBase(); + + var metadataElement = rootDocument.Root.ElementsNoNamespace("metadata").FirstOrDefault(); + + if (metadataElement == null) + { + var message = string.Format(CultureInfo.InvariantCulture, Messages.NuspecRequiredXmlElementMissing, "metadata"); + throw new InvalidDataException(message); + } + + ReadPackageMetadata(ref pkg, metadataElement); + + return pkg; + } + + /// + /// A help for processing the metatadata tag + /// + /// + /// + private static void ReadPackageMetadata(ref PackageBase package, XElement xElement) + { + var node = xElement.FirstNode; + + if (node == null) { + + //Check for the tags within metadata tag + throw new InvalidDataException(string.Format(CultureInfo.InvariantCulture, Messages.NuspecRequiredXmlElementMissing, "id, version, authors, or description")); + } + + while (node != null) + { + var element = node as XElement; + if (element != null) + { + ReadPackageMetadataElement(ref package, element); + } + node = node.NextNode; + } + + // now check for required elements, which include , , and + EnsureRequiredXmlTags(package); + } + + /// + /// A helper method for processing the children of the 'metadata' tag in the .nuspec file + /// + /// + /// + private static void ReadPackageMetadataElement(ref PackageBase package, XElement element) + { + var value = element.Value.SafeTrim(); + + // since we put all the name to lower case, each word in case should all be in lower case + switch (element.Name.LocalName.ToLowerInvariant()) + { + case "id": + package.Id = value; + break; + case "version": + package.Version = value; + break; + case "authors": + package.Authors = value; + break; + case "owners": + package.Owners = value; + break; + case "licenseurl": + package.LicenseUrl = string.IsNullOrWhiteSpace(value) ? null : new Uri(value); + break; + case "projecturl": + package.ProjectUrl = string.IsNullOrWhiteSpace(value) ? null : new Uri(value); + break; + case "iconurl": + package.IconUrl = string.IsNullOrWhiteSpace(value) ? null : new Uri(value); + break; + case "requirelicenseacceptance": + package.RequireLicenseAcceptance = XmlConvert.ToBoolean(value); + break; + case "developmentdependency": + package.DevelopmentDependency = XmlConvert.ToBoolean(value); + break; + case "description": + package.Description = value; + break; + case "summary": + package.Summary = value; + break; + case "releasenotes": + package.ReleaseNotes = value; + break; + case "copyright": + package.Copyright = value; + break; + case "language": + package.Language = value; + break; + case "title": + package.Title = value; + break; + case "tags": + package.Tags = value; + break; + case "dependencies": + package.DependencySetList = ReadDependencyList(element); + break; + /* case "frameworkAssemblies": + manifestMetadata.FrameworkAssemblies = ReadFrameworkAssemblies(element); + break; + case "references": + manifestMetadata.ReferenceSets = ReadReferenceSets(element); + break;*/ + } + } + + /// + /// Parse dependencies from the xelement and returns a list of PackageDependencySet + /// + /// + /// + private static List ReadDependencyList(XElement dependenciesElement) + { + // No elements so return empty list + if (!dependenciesElement.HasElements) + { + return new List(); + } + + // Direct child of dependenciesElement with tag + var groups = dependenciesElement.ElementsNoNamespace("group"); + + // It is an error for element to contain both and child elements + if (dependenciesElement.ElementsNoNamespace("dependency").Any() && groups.Any()) + { + throw new InvalidDataException(Messages.DependencyHasBothElements); + } + + var dependencies = ReadDependencies(dependenciesElement); + + if (!groups.Any()) + { + // since there is no group, we are encoutering + // old format, is direct child of + var dependencySet = new PackageDependencySet + { + Dependencies = dependencies + }; + + return new List { dependencySet }; + } + else + { + // new format, with as child of + // Project each group into a packagedependencyset + return groups.Select(group => + new PackageDependencySet { + TargetFramework = group.GetOptionalAttributeValue("targetFramework").SafeTrim(), + Dependencies = ReadDependencies(group) + }).ToList(); + } + } + + /// + /// Read the dependencies from xelement. Returns a list of dependencies + /// + /// + /// + private static List ReadDependencies(XElement containerElement) + { + // list of dependency + var dependencies = containerElement.ElementsNoNamespace("dependency"); + + return (from element in containerElement.ElementsNoNamespace("dependency") + let idElement = element.Attribute("id") + // Check that id is not null or empty + where idElement != null && !String.IsNullOrEmpty(idElement.Value) + // Project a PackageDependency based on that + select new PackageDependency + { + Id = idElement.Value.SafeTrim(), + DependencyVersion = DependencyVersion.ParseDependencyVersion(element.GetOptionalAttributeValue("version").SafeTrim()) + }).ToList(); + } + + /// + /// Make sure the Nuget required xml tags exist witin the metadata tag in the nuspec file. + /// + /// + private static void EnsureRequiredXmlTags(PackageBase package) + { + //As a .nupec, id", "version", "authors", "description" tags are required. + //Check if they exists + + if (string.IsNullOrWhiteSpace(package.Version) || + string.IsNullOrWhiteSpace(package.Id) || + string.IsNullOrWhiteSpace(package.Authors) || + string.IsNullOrWhiteSpace(package.Description)) + { + //bad .nuspec xml format + string message = string.Format(CultureInfo.InvariantCulture, "{0}: '{1}', '{2}, '{3}', '{4}'", Messages.NuspecRequiredXmlElementMissing, "Version", "Id", "authors", "description"); + + throw new InvalidDataException(message); + } + } + + /// + /// Unzip the file + /// + /// The file to be unzipped. + /// + internal static string DecompressFile(string fullZipPath) { + + var extractedFolder = Path.Combine(Path.GetDirectoryName(fullZipPath), Path.GetFileNameWithoutExtension(fullZipPath)); + + //this possible race condition if someone else is creating it. + if (Directory.Exists(extractedFolder)) { + Directory.Delete(extractedFolder, true); + } + + ZipFile.ExtractToDirectory(fullZipPath, extractedFolder); + + //Delete the default files that come with the packages _rels, [Content_Types].xml, and packages folder + FileUtility.DeleteDirectory(Path.Combine(extractedFolder, "_rels"), true, false); + + //var files = Directory.GetFiles(extractedFolder, "[Content_Types].xml", SearchOption.TopDirectoryOnly); + FileUtility.DeleteFile(Path.Combine(extractedFolder, "[Content_Types].xml"), false); + + //e.g ..\package\services\metadata\core-properties\341424.psmdcp + var metadataFile = Path.Combine(extractedFolder, "package", "services", "metadata", "core-properties"); + if (Directory.Exists(metadataFile)) { + FileUtility.DeleteDirectory(Path.Combine(extractedFolder, "package"), true, false); + } + + return extractedFolder; + } + + /// + /// Unzip the .nupkg file and read the .nuspec manifest. + /// + /// The nupkg file + /// Package name + /// + internal static PackageBase DecompressFile(string nupkgFilePath, string packageName) + { + if (!File.Exists(nupkgFilePath)) + { + throw new FileNotFoundException(nupkgFilePath); + } + + XDocument root = ReadNuSpecFromNuPkg(nupkgFilePath); + + return ProcessNuspec(root); + } + + /// + /// Read .nuspec manifest from the .nupkg package file. + /// + /// The nupkg file path. + /// + private static XDocument ReadNuSpecFromNuPkg(string nupkgPath) + { + if (nupkgPath == null) + { + throw new ArgumentNullException("nupkgPath"); + } + + // We set FileAccess.Read here to ensure a user only needs tne read access permission for find packages. + // By default, FileStream needs ReadWrite access that’s why + // System.UnauthorizedAccessException is thrown if a user does not have write access. + using (FileStream zipToOpen = new FileStream(nupkgPath, FileMode.Open, FileAccess.Read)) + { + using (ZipArchive archive = new ZipArchive(zipToOpen, ZipArchiveMode.Read)) + { + var entry = archive.Entries.FirstOrDefault(each => each.Name.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase) && + each.FullName.IndexOf("/", StringComparison.OrdinalIgnoreCase) == -1); + if (entry != null) { + return XDocument.Load(entry.Open()); + } + } + } + return null; + } + + } +} diff --git a/src/Microsoft.PackageManagement.NuGetProvider/Utility/ProgressTracker.cs b/src/Microsoft.PackageManagement.NuGetProvider/Utility/ProgressTracker.cs new file mode 100644 index 000000000..a59a031ba --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/Utility/ProgressTracker.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PackageManagement.NuGetProvider.Utility +{ + internal class ProgressTracker + { + internal int ProgressID; + internal int StartPercent; + internal int EndPercent; + + internal ProgressTracker(int progressID) : this(progressID, 0, 100) { } + + internal ProgressTracker(int progressID, int startPercent, int endPercent) + { + ProgressID = progressID; + StartPercent = startPercent; + EndPercent = endPercent; + } + + internal int ConvertPercentToProgress(double percent) + { + // for example, if startprogress is 50, end progress is 100, and if we want to complete 50% of that, + // then the progress returned would be 75 + return StartPercent + (int)((EndPercent - StartPercent) * percent); + } + + internal static ProgressTracker StartProgress(ProgressTracker parentTracker, string message, NuGetRequest request) + { + if (request == null) + { + return null; + } + + // if parent tracker is null, use 0 for parent id, else use the progressid of parent tracker + return new ProgressTracker(request.StartProgress(parentTracker == null ? 0 : parentTracker.ProgressID, message)); + } + } +} diff --git a/src/Microsoft.PackageManagement.NuGetProvider/nugetlightprovider.cs b/src/Microsoft.PackageManagement.NuGetProvider/nugetlightprovider.cs new file mode 100644 index 000000000..b21a211ca --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/nugetlightprovider.cs @@ -0,0 +1,622 @@ +using Microsoft.PackageManagement.Provider.Utility; + +namespace Microsoft.PackageManagement.NuGetProvider +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Management.Automation; + + /// + /// A Package provider to the PackageManagement Platform. + /// + /// Important notes: + /// - Required Methods: Not all methods are required; some package providers do not support some features. If the methods isn't used or implemented it should be removed (or commented out) + /// - Error Handling: Avoid throwing exceptions from these methods. To properly return errors to the user, use the request.Error(...) method to notify the user of an error conditionm and then return. + /// - Communicating with the HOST and CORE: each method takes a Request (in reality, an alias for System.Object), which can be used in one of two ways: + /// - use the c# 'dynamic' keyword, and call functions on the object directly. + /// - use the () ]]> extension method to strongly-type it to the Request type (which calls upon the duck-typer to generate a strongly-typed wrapper). + /// The strongly-typed wrapper also implements several helper functions to make using the request object easier. + /// + /// + public class NuGetProvider + { + /// + /// The features that this package supports. + /// + internal static readonly Dictionary Features = new Dictionary { + + //Required by PowerShellGet provider + { Constants.Features.SupportsPowerShellModules, Constants.FeaturePresent }, + + // specify the extensions that your provider uses for its package files (if you have any) + { Constants.Features.SupportedExtensions, new[]{"nupkg"}}, + + // you can list the URL schemes that you support searching for packages with + { Constants.Features.SupportedSchemes, new []{"http", "https", "file"}}, + + // you can list the magic signatures (bytes at the beginning of a file) that we can use + // to peek and see if a given file is yours. + { Constants.Features.MagicSignatures, new[]{Constants.Signatures.Zip}}, + + }; + + /// + /// Returns the name of the Provider. + /// + /// The name of this provider + public string PackageProviderName { + get { return NuGetConstant.ProviderName; } + } + + /// + /// Returns the version of the Provider. + /// + /// The version of this provider + public string ProviderVersion { + + get{return NuGetConstant.ProviderVersion;} + } + + /// + /// This is just here as to give us some possibility of knowing when an unexception happens... + /// At the very least, we'll write it to the system debug channel, so a developer can find it if they are looking for it. + /// + public void OnUnhandledException(string methodName, Exception exception) { + if (exception == null){ + throw new ArgumentNullException("exception"); + } + + if (!string.IsNullOrWhiteSpace(methodName)) + { + Debug.WriteLine("Unexpected Exception thrown in '{0}::{1}' -- {2}\\{3}\r\n{4}", PackageProviderName, methodName, "OnUnhandledException", exception.Message, exception.StackTrace); + } else { + Debug.WriteLine("Unexpected Exception thrown in '{0}' -- {1}\\{2}\r\n{3}", PackageProviderName, "OnUnhandledException", exception.Message, exception.StackTrace); + } + } + + /// + /// Performs one-time initialization of the $provider. + /// + /// An object passed in from the CORE that contains functions that can be used to interact with the CORE and HOST + public void InitializeProvider(NuGetRequest request) + { + if (request == null){ + throw new ArgumentNullException("request"); + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, "InitializeProvider"); + } + + /// + /// Returns a collection of strings to the client advertizing features this provider supports. + /// + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + public void GetFeatures(Request request) + { + if (request == null){ + throw new ArgumentNullException("request"); + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, "GetFeatures"); + + request.Yield(Features); + } + + /// + /// Returns dynamic option definitions to the HOST + /// + /// example response: + /// request.YieldDynamicOption( "MySwitch", OptionType.String.ToString(), false); + /// + /// + /// The category of dynamic options that the HOST is interested in + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + public void GetDynamicOptions(string category, Request request) + { + if (request == null){ + throw new ArgumentNullException("request"); + } + + if (category == null) + { + request.Warning(Resources.Messages.UnkownCategory, PackageProviderName, "GetDynamicOptions", category); + return; + } + + switch ((category ?? string.Empty).ToLowerInvariant()) { + case "install": + // put any options required for install/uninstall/getinstalledpackages + + request.YieldDynamicOption("Destination", Constants.OptionType.Folder, false); + request.YieldDynamicOption("ExcludeVersion", Constants.OptionType.Switch, false); + request.YieldDynamicOption("Scope", Constants.OptionType.String, false, new[] { Constants.CurrentUser, Constants.AllUsers }); + //request.YieldDynamicOption("SkipDependencies", Constants.OptionType.Switch, false); + //request.YieldDynamicOption("ContinueOnFailure", Constants.OptionType.Switch, false); + break; + + case "source": + // put any options for package sources + request.YieldDynamicOption("ConfigFile", Constants.OptionType.String, false); + request.YieldDynamicOption("SkipValidate", Constants.OptionType.Switch, false); + break; + + case "package": + // put any options used when searching for packages + request.YieldDynamicOption("Headers", Constants.OptionType.StringArray, false); + request.YieldDynamicOption("FilterOnTag", Constants.OptionType.StringArray, false); + request.YieldDynamicOption("Contains", Constants.OptionType.String, false); + request.YieldDynamicOption("AllowPrereleaseVersions", Constants.OptionType.Switch, false); + break; + default: + request.Warning(Resources.Messages.UnkownCategory, PackageProviderName, "GetDynamicOptions", category); + break; + } + } + + /// + /// Resolves and returns Package Sources to the client. + /// + /// Specified sources are passed in via the request object (request.GetSources()). + /// + /// Sources are returned using request.YieldPackageSource(...) + /// + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + public void ResolvePackageSources(NuGetRequest request) + { + if (request == null){ + throw new ArgumentNullException("request"); + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, "ResolvePackageSources"); + + var selectedSources = request.SelectedSources; + + try + { + foreach (var source in selectedSources) + { + request.Debug(Resources.Messages.YieldingPackageSource, PackageProviderName, source); + request.YieldPackageSource(source.Name, source.Location, source.Trusted, source.IsRegistered, source.IsValidated); + } + } + catch (Exception e) + { + e.Dump(request); + } + + request.Debug(Resources.Messages.DebugInfoReturnCall, PackageProviderName, "ResolvePackageSources"); + } + + /// + /// This is called when the user is adding (or updating) a package source + /// + /// If this PROVIDER doesn't support user-defined package sources, remove this method. + /// + /// The name of the package source. If this parameter is null or empty the PROVIDER should use the location as the name (if the PROVIDER actually stores names of package sources) + /// The location (ie, directory, URL, etc) of the package source. If this is null or empty, the PROVIDER should use the name as the location (if valid) + /// A boolean indicating that the user trusts this package source. Packages returned from this source should be marked as 'trusted' + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + public void AddPackageSource(string name, string location, bool trusted, NuGetRequest request) + { + if (request == null){ + throw new ArgumentNullException("request"); + } + try { + request.Debug(string.Format(CultureInfo.InvariantCulture, "AddPackageSource - ProvidenName = '{0}', name='{1}', location='{2}', trusted='{3}'", PackageProviderName, name, location, trusted)); + + // Error out if a user does not provide package source Name + if (string.IsNullOrWhiteSpace(name)) + { + request.WriteError(ErrorCategory.InvalidArgument, Constants.Parameters.Name, Constants.Messages.MissingRequiredParameter, Constants.Parameters.Name); + return; + } + + if (string.IsNullOrWhiteSpace(location)) + { + request.WriteError(ErrorCategory.InvalidArgument, Constants.Parameters.Location, Constants.Messages.MissingRequiredParameter, Constants.Parameters.Location); + return; + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, "GetOptionValue"); + // if this is supposed to be an update, there will be a dynamic parameter set for IsUpdatePackageSource + var isUpdate = request.GetOptionValue(Constants.Parameters.IsUpdate).IsTrue(); + + request.Debug(Resources.Messages.VariableCheck, "IsUpdate", isUpdate); + + // if your source supports credentials you get get them too: + // string username =request.Username; + // SecureString password = request.Password; + // feel free to send back an error here if your provider requires credentials for package sources. + + // check first that we're not clobbering an existing source, unless this is an update + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, string.Format(CultureInfo.InvariantCulture, "FindRegisteredSource -name'{0}'", name)); + + var src = request.FindRegisteredSource(name); + + if (src != null && !isUpdate) + { + // tell the user that there's one here already + request.WriteError(ErrorCategory.InvalidArgument, name, Constants.Messages.PackageSourceExists, name); + return; + } + + // conversely, if it didn't find one, and it is an update, that's bad too: + if (src == null && isUpdate) + { + // you can't find that package source? Tell that to the user + request.WriteError(ErrorCategory.ObjectNotFound, name, Constants.Messages.UnableToResolveSource, name); + return; + } + + // ok, we know that we're ok to save this source + // next we check if the location is valid (if we support that kind of thing) + + var validated = false; + + if (!request.SkipValidate.Value) + { + // the user has not opted to skip validating the package source location, so check if the source is valid + validated = request.ValidateSourceLocation(location); + + if (!validated) + { + request.WriteError(ErrorCategory.InvalidData, name, Constants.Messages.SourceLocationNotValid, location); + return; + } + + request.Verbose(Resources.Messages.SuccessfullyValidated, name); + } + + // it's good to check just before you actaully write something to see if the user has cancelled the operation + if (request.IsCanceled) + { + return; + } + + // looking good -- store the package source. + request.AddPackageSource(name, location, trusted, validated); + + // Yield the package source back to the caller. + request.YieldPackageSource(name, location, trusted, true /*since we just registered it*/, validated); + } + catch (Exception e) + { + e.Dump(request); + } + } + + /// + /// Removes/Unregisters a package source + /// + /// The name or location of a package source to remove. + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + public void RemovePackageSource(string name, NuGetRequest request) + { + if (request == null){ + throw new ArgumentNullException("request"); + } + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, "RemovePackageSource"); + + var src = request.FindRegisteredSource(name); + if (src == null) + { + request.Warning(Constants.Messages.UnableToResolveSource, name); + return; + } + + request.RemovePackageSource(src.Name); + request.YieldPackageSource(src.Name, src.Location, src.Trusted, false, src.IsValidated); + } + + /// + /// Searches package sources given name and version information + /// + /// Package information must be returned using request.YieldPackage(...) function. + /// + /// a name or partial name of the package(s) requested + /// A specific version of the package. Null or empty if the user did not specify + /// A minimum version of the package. Null or empty if the user did not specify + /// A maximum version of the package. Null or empty if the user did not specify + /// if this is greater than zero (and the number should have been generated using StartFind(...), the core is calling this multiple times to do a batch search request. The operation can be delayed until CompleteFind(...) is called + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + public void FindPackage(string name, string requiredVersion, string minimumVersion, string maximumVersion, int id, NuGetRequest request) + { + if (request == null){ + throw new ArgumentNullException("request"); + } + + // true if we want to include the max and min version + bool minInclusive = true; + bool maxInclusive = true; + + // If finding by canonical id, then the version follows dependency version requirement + if (request.GetOptionValue("FindByCanonicalId").IsTrue()) + { + // Use the dependency version if no min and max is supplied + if (String.IsNullOrWhiteSpace(maximumVersion) && String.IsNullOrWhiteSpace(minimumVersion)) + { + DependencyVersion depVers = DependencyVersion.ParseDependencyVersion(requiredVersion); + maximumVersion = depVers.MaxVersion.ToStringSafe(); + minimumVersion = depVers.MinVersion.ToStringSafe(); + minInclusive = depVers.IsMinInclusive; + maxInclusive = depVers.IsMaxInclusive; + + // set required version if we have both min max as the same value. + if (depVers.MaxVersion != null && depVers.MinVersion != null + && depVers.MaxVersion == depVers.MinVersion && minInclusive && maxInclusive) + { + requiredVersion = maximumVersion; + } + else + { + requiredVersion = null; + } + } + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, string.Format(CultureInfo.InvariantCulture, "FindPackage' - name='{0}', requiredVersion='{1}',minimumVersion='{2}', maximumVersion='{3}'", name, requiredVersion, minimumVersion, maximumVersion)); + + NormalizeVersion(request, ref requiredVersion, ref minimumVersion, ref maximumVersion); + + try { + + + // If there are any packages, yield and return + if (request.YieldPackages(request.GetPackageById(name, request, requiredVersion, minimumVersion, maximumVersion, minInclusive, maxInclusive), name)) + { + return; + } + + // Check if the name contains wildcards. If not, return. This matches the behavior as "Get-module xje" + if (!String.IsNullOrWhiteSpace(name) && !WildcardPattern.ContainsWildcardCharacters(name)) + { + return; + } + + // In the case of the package name is null or contains wildcards, error out if a user puts version info + if (!String.IsNullOrWhiteSpace(requiredVersion) || !String.IsNullOrWhiteSpace(minimumVersion) || !String.IsNullOrWhiteSpace(maximumVersion)) + { + request.Warning( Constants.Messages.MissingRequiredParameter, "name"); + return; + } + + + + // Have we been cancelled? + if (request.IsCanceled) { + request.Debug(Resources.Messages.RequestCanceled, PackageProviderName, "FindPackage"); + + return; + } + + // A user does not provide the package full Name at all Or used wildcard in the name. Let's try searching the entire reposiotry for matches. + request.YieldPackages(request.SearchForPackages(name), name); + } + catch (Exception ex) + { + ex.Dump(request); + } + } + + /// + /// Finds packages given a locally-accessible filename + /// + /// Package information must be returned using request.YieldPackage(...) function. + /// + /// the full path to the file to determine if it is a package + /// if this is greater than zero (and the number should have been generated using StartFind(...), the core is calling this multiple times to do a batch search request. The operation can be delayed until CompleteFind(...) is called + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + public void FindPackageByFile(string file, int id, NuGetRequest request) + { + if (request == null){ + throw new ArgumentNullException("request"); + } + + request.Debug(Resources.Messages.DebugInfoCallMethod3, PackageProviderName, file, id); + + var pkgItem = request.GetPackageByFilePath(Path.GetFullPath(file)); + if (pkgItem != null) + { + request.YieldPackage(pkgItem, file); + } + } + + /// + /// Downloads a remote package file to a local location. + /// + /// + /// + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + public void DownloadPackage(string fastPackageReference, string destLocation, NuGetRequest request) + { + if (request == null){ + throw new ArgumentNullException("request"); + } + + request.Debug(Resources.Messages.DebugInfoCallMethod3, PackageProviderName, fastPackageReference, destLocation); + + try { + var pkgItem = request.GetPackageByFastpath(fastPackageReference); + if (pkgItem == null) { + request.WriteError(ErrorCategory.InvalidArgument, fastPackageReference, Constants.Messages.UnableToResolvePackage); + return; + } + + NuGetClient.InstallOrDownloadPackageHelper(pkgItem, request, Constants.Download, + (packageItem, progressTracker) => NuGetClient.DownloadSinglePackage(packageItem, request, destLocation, progressTracker)); + } catch (Exception ex) { + ex.Dump(request); + request.WriteError(ErrorCategory.InvalidOperation, fastPackageReference, ex.Message); + } + } + + /// + /// Installs a given package. + /// + /// A provider supplied identifier that specifies an exact package + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + public void InstallPackage(string fastPackageReference, NuGetRequest request) + { + if (request == null){ + throw new ArgumentNullException("request"); + } + + request.Debug(Resources.Messages.DebugInfoCallMethod3, PackageProviderName, "InstallPackage", fastPackageReference); + + try { + + var pkgItem = request.GetPackageByFastpath(fastPackageReference); + if (pkgItem == null) { + request.WriteError(ErrorCategory.InvalidArgument, fastPackageReference, Constants.Messages.UnableToResolvePackage); + return; + } + + if ((pkgItem.PackageSource == null) || (pkgItem.PackageSource.Location == null) || (pkgItem.Package == null)) { + request.Debug(Resources.Messages.VariableCheck, "PackageSource or PackageSource.Location or Package object", "null"); + request.WriteError(ErrorCategory.ObjectNotFound, fastPackageReference, Constants.Messages.UnableToResolvePackage, pkgItem.Id); + return; + } + + request.Debug(Resources.Messages.VariableCheck, "Package version", pkgItem.Version); + request.Debug(Resources.Messages.VariableCheck, "Request's Destination", request.Destination); + + // got this far, let's install the package we came here for. + if (!NuGetClient.InstallOrDownloadPackageHelper(pkgItem, request, Constants.Install, + (packageItem, progressTracker) => NuGetClient.InstallSinglePackage(packageItem, request, progressTracker))) + { + // package itself didn't install. Write error + request.WriteError(ErrorCategory.InvalidResult, pkgItem.Id, Constants.Messages.PackageFailedInstallOrDownload, pkgItem.Id, CultureInfo.CurrentCulture.TextInfo.ToLower(Constants.Install)); + + return; + } + } catch (Exception ex) { + ex.Dump(request); + request.WriteError(ErrorCategory.InvalidOperation, fastPackageReference, ex.Message); + } + } + + /// + /// Uninstalls a package + /// + /// + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + public void UninstallPackage(string fastPackageReference, NuGetRequest request) + { + if (request == null){ + throw new ArgumentNullException("request"); + } + + request.Debug(Resources.Messages.DebugInfoCallMethod3, PackageProviderName, "UninstallPackage", fastPackageReference); + + var pkg = request.GetPackageByFastpath(fastPackageReference); + + NuGetClient.UninstallPackage(request, pkg); + } + + /// + /// Returns the packages that are installed. This method is called when a user type get-package, install-package and uninstall-package. + /// + /// the package name to match. Empty or null means match everything + /// the specific version asked for. If this parameter is specified (ie, not null or empty string) then the minimum and maximum values are ignored + /// the minimum version of packages to return . If the requiredVersion parameter is specified (ie, not null or empty string) this should be ignored + /// the maximum version of packages to return . If the requiredVersion parameter is specified (ie, not null or empty string) this should be ignored + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + public void GetInstalledPackages(string name, string requiredVersion, string minimumVersion, string maximumVersion, NuGetRequest request) + { + if (request == null){ + throw new ArgumentNullException("request"); + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, string.Format(CultureInfo.InvariantCulture, "GetInstalledPackages' - name='{0}', requiredVersion='{1}',minimumVersion='{2}', maximumVersion='{3}'", name, requiredVersion, minimumVersion, maximumVersion)); + + // In the case of the package name is null or contains wildcards, error out if a user puts version info + if (!String.IsNullOrWhiteSpace(requiredVersion) || !String.IsNullOrWhiteSpace(minimumVersion) || !String.IsNullOrWhiteSpace(maximumVersion)) + { + // A user provides the version info but missing name + if (string.IsNullOrWhiteSpace(name)) + { + request.Warning(Constants.Messages.MissingRequiredParameter, "name"); + return; + } + + // A user provides the version info but the name containing wildcards + if (WildcardPattern.ContainsWildcardCharacters(name)) { + return; + } + } + + NormalizeVersion(request, ref requiredVersion, ref minimumVersion, ref maximumVersion); + + request.GetInstalledPackages(name, requiredVersion, minimumVersion, maximumVersion); + } + + private static void NormalizeVersion(NuGetRequest request, ref string requiredVersion, ref string minimumVersion, ref string maximumVersion) { + if (!string.IsNullOrWhiteSpace(requiredVersion)) + { + requiredVersion = requiredVersion.FixVersion(); + minimumVersion = null; + maximumVersion = null; + } + else + { + if (!string.IsNullOrWhiteSpace(minimumVersion)) + { + minimumVersion = minimumVersion.FixVersion(); + } + + if (!string.IsNullOrWhiteSpace(maximumVersion)) + { + maximumVersion = maximumVersion.FixVersion(); + } + } + + if (!string.IsNullOrWhiteSpace(minimumVersion) && !string.IsNullOrWhiteSpace(maximumVersion)) + { + if(new SemanticVersion(minimumVersion) > new SemanticVersion(maximumVersion)) + { + request.Warning("Specified version range is invalid. minimumVersion = {0} maximumVersion ={1}", minimumVersion, maximumVersion); + } + } + } + + /// + /// True, if the package matches. + /// + /// Package name + /// Package file Path + /// + internal static bool IsNameMatch(string packageName, string filePath) + { + // Get the file name + var newName = Path.GetFileNameWithoutExtension(filePath); + + // Strip off the version info from the file name, e.g., Jquery.2.1.3 + newName = PackageUtility.GetPackageNameWithoutVersionInfo(newName); + + if (WildcardPattern.ContainsWildcardCharacters(packageName)) + { + // Applying the wildcard pattern matching + const WildcardOptions wildcardOptions = WildcardOptions.CultureInvariant | WildcardOptions.IgnoreCase; + var wildcardPattern = new WildcardPattern(packageName, wildcardOptions); + + return wildcardPattern.IsMatch(newName); + + } + + if (string.IsNullOrWhiteSpace(newName)) + { + return false; + } + + var reval = (newName.Equals(packageName, StringComparison.OrdinalIgnoreCase)); + + return reval; + } + } +} diff --git a/src/Microsoft.PackageManagement.NuGetProvider/provider.manifest b/src/Microsoft.PackageManagement.NuGetProvider/provider.manifest new file mode 100644 index 000000000..576f1449d --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/provider.manifest @@ -0,0 +1,8 @@ + + + + diff --git a/src/Microsoft.PackageManagement.NuGetProvider/resources/Microsoft.PackageManagement.NuGetProvider.Resources.Messages.resx b/src/Microsoft.PackageManagement.NuGetProvider/resources/Microsoft.PackageManagement.NuGetProvider.Resources.Messages.resx new file mode 100644 index 000000000..164a80cb6 --- /dev/null +++ b/src/Microsoft.PackageManagement.NuGetProvider/resources/Microsoft.PackageManagement.NuGetProvider.Resources.Messages.resx @@ -0,0 +1,336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Package '{0}' is already installed. + + + Completed downloading '{0}'. + + + Completed iterating for '{0}'. + + + Copying unzipped package to '{0}' + + + Could not get response from query '{0}'. + + + Create directory '{0}'. + + + Create a file '{0}'. + + + Calling '{0}'::'{1}'. + + + Calling '{0}'::'{1}', '{2}'. + + + Returning the call '{0}'::'{1}'. + + + Dependency cannot have both dependency and group elements. + + + Dependency loop detected for package '{0}'. + + + Dependent Package '{0}' failed to {1}. + + + Downloading '{0}'. + + + Downloaded {0:0.00} MB out of {1:0.00} MB. + + + Downloading package '{0}' failed, please make sure '{1}' is accessable. + + + File '{0}' does not exist in '{1}'. + + + Finish installing package '{0}' + + + Source '{0}' is one of the registered sources in '{1}' provider. + + + Found '{0}' registered package source in '{1}' provider. + + + Hash not found for the package '{0}'. + + + Hash for package '{0}' does not match hash provided from the server. + + + Hash algorithm '{0}' is not supported. + + + Provided header value '{0}' was invalid and hence ignored. + + + Download is incomplete. Downloaded '{0}' out of '{0}' bytes. + + + {0}ed dependent package '{1}' + + + {0}ing dependent package '{1}' + + + {0}ing package '{1}' + + + The file extension '{0}' is not valid. The required file extension is '.nupkg'. + + + The package '{0}' does not have the .nuspec file. + + + The property of PackageSaveMode specified '{0}' is invalid. + + + Query Url {0} is invalid. + + + '{0}' is not a valid version string. + + + Iterating '{0}'. + + + Loaded a configuration from file '{0}'. + + + Loading a configuration from file '{0}'. + + + The required xml element '{0}' is missing from the manifest. + + + Configuration file '{0}' does not have 'configuration' element. + + + Source '{0}' is not one of the registered sources in '{1}' provider. + + + The required xml element '{0}' is missing from the nuspec. + + + Package '{0}' failed to {1}. + + + Package '{0}' failed to be installed because: {1} + + + '{0}' packages recevied in the last request. + + + Process Package Manifest + + + There are '{0}' registered sources in '{1}' provider. + + + Removed the package source: '{0}'. + + + Request is canceled in '{0}'::'{1}'. + + + Retry downloading '{0}' for '{1}' more times + + + Saving a config with a file path '{0}'. + + + Searching repository '{0}' for '{1}'. + + + Setting a config object to null. + + + Skipping Downloaded Package '{0}'. + + + Source '{0}' is a directory. + + + Source '{0}' is a file path. + + + Source Location '{0}' is not valid. + + + Package '{0}' is downloaded. + + + Package '{0}' is installed. + + + Source '{0}' is validated. + + + Total package yield:'{0}' for the specified package '{1}'. + + + Type to compare must be an instance of SemanticVersion. + + + The package '{0}' failed to install. + + + Unable to resolve package dependency -- package '{0}' not found. + + + Unable to resolve package source '{0}'. + + + The file extension '{0}'::'{1}' is not valid. The required file extension is '.nuspec' or '.nupkg'. + + + Uninstalled '{0}' package. + + + Unknown category for '{0}'::'{1}': '{2}' + + + Unzipping + + + Uri Scheme '{0}' is not supported. + + + Use the default configuration. + + + '{0}' is '{1}'. + + + Yielding. + + + Yielding the package source: '{0}'. + + \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.PackageSourceListProvider/Extensions/PackageListExtensions.cs b/src/Microsoft.PackageManagement.PackageSourceListProvider/Extensions/PackageListExtensions.cs new file mode 100644 index 000000000..f57e4ab32 --- /dev/null +++ b/src/Microsoft.PackageManagement.PackageSourceListProvider/Extensions/PackageListExtensions.cs @@ -0,0 +1,92 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Reflection; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.PackageManagement.Internal.Implementation; + +namespace Microsoft.PackageManagement.PackageSourceListProvider +{ + + internal static class ExceptionExtensions + { + internal static void Dump(this Exception ex, Request request) + { + request.Debug(ex.ToString()); + } + } + + internal static class StringExtensions + { + // ReSharper disable InconsistentNaming + /// + /// Formats the specified format string. + /// + /// The format string. + /// The args. + /// + /// + /// + public static string format(this string formatString, params object[] args) + { + if (args == null || args.Length == 0) + { + return formatString; + } + + try + { + var replacedByName = false; + // first, try to replace + formatString = new Regex(@"\$\{(?\w*?)\}").Replace(formatString, new MatchEvaluator((m) => { + var key = m.Groups["macro"].Value; + + var p = args[0].GetType().GetTypeInfo().GetProperty(key); + if (p != null) + { + replacedByName = true; + return p.GetValue(args[0], null).ToString(); + } + return "${{" + m.Groups["macro"].Value + "}}"; + })); + + // if it looks like it doesn't take parameters, (and yet we have args!) + // let's return a fix-me-format string. + if (!replacedByName && formatString.IndexOf('{') < 0) + { + return FixMeFormat(formatString, args); + } + + return String.Format(CultureInfo.CurrentCulture, formatString, args); + } + catch (Exception) + { + // if we got an exception, let's at least return a string that we can use to figure out what parameters should have been matched vs what was passed. + return FixMeFormat(formatString, args); + } + } + + private static string FixMeFormat(string formatString, object[] args) + { + if (args == null || args.Length == 0) + { + // not really any args, and not really expectng any + return formatString.Replace('{', '\u00ab').Replace('}', '\u00bb'); + } + return args.Aggregate(formatString.Replace('{', '\u00ab').Replace('}', '\u00bb'), (current, arg) => current + string.Format(CultureInfo.CurrentCulture, " \u00ab{0}\u00bb", arg)); + } + + } + + internal static class DictionaryExtensions + { + internal static TValue Get(this IDictionary dictionary, TKey key) + { + return dictionary.ContainsKey(key) ? dictionary[key] : default(TValue); + } + } +} diff --git a/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/ExePackageInstaller.cs b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/ExePackageInstaller.cs new file mode 100644 index 000000000..ea8b9bcbf --- /dev/null +++ b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/ExePackageInstaller.cs @@ -0,0 +1,594 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.PackageSourceListProvider +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Management.Automation; + using System.Security.AccessControl; + using System.Threading; + using Microsoft.PackageManagement.Internal.Implementation; + using Microsoft.PackageManagement.Provider.Utility; + using Microsoft.Win32; + using ErrorCategory = PackageManagement.Internal.ErrorCategory; + + internal static class ExePackageInstaller + { + internal static bool InstallExePackage(PackageJson package, string fastPath, PackageSourceListRequest request) { + + ProgressTracker tracker = new ProgressTracker(request.StartProgress(0, Resources.Messages.Installing)); + + var exePackage = Path.ChangeExtension(Path.GetTempFileName(), "exe"); + WebDownloader.DownloadFile(package.Source, exePackage, request, tracker); + + if (File.Exists(exePackage)) { + request.Verbose("Package: '{0}'", exePackage); + + // validate the file + if (!WebDownloader.PerformSecurityScan(exePackage)) + { + //TODO Security Scan work + request.WriteError(ErrorCategory.InvalidOperation, Constants.ProviderName, "The package download from '{0}' failed in the security scan.", package.Source); + return false; + } + + if (!package.IsTrustedSource) + { + if (!request.ShouldContinueWithUntrustedPackageSource(package.Name, package.Source)) + { + request.Warning(Constants.Messages.UserDeclinedUntrustedPackageInstall, package.Name); + return false; + } + } + + // Prepare the process to run + var processArguments = string.IsNullOrWhiteSpace(package.InstallArguments) + ? "/VERYSILENT /CLOSEAPPLICATIONS /NORESTART /NOCANCEL /SP /qn" + : package.InstallArguments; + + var start = new ProcessStartInfo { + FileName = exePackage, + Arguments = processArguments, + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + //LoadUserProfile = true, + }; + + + double percent = tracker.StartPercent; + Timer timer = null; + object timerLock = new object(); + bool cleanUp = false; + + Action cleanUpAction = () => { + lock (timerLock) { + // check whether clean up is already done before or not + if (!cleanUp) { + try { + if (timer != null) { + // stop timer + timer.Change(Timeout.Infinite, Timeout.Infinite); + // dispose it + timer.Dispose(); + } + } catch { + } + + cleanUp = true; + } + } + }; + + // Run the external process & wait for it to finish + using (var proc = Process.Start(start)) { + var timer1 = timer; + timer = new Timer(_ => { + percent += 0.025; + + // percent between startProgress and endProgress + var progressPercent = tracker.ConvertPercentToProgress(percent); + if (progressPercent < 90) { + request.Progress(tracker.ProgressID, (int)progressPercent, Resources.Messages.InstallingPackage, package.Source); + } + if (request.IsCanceled) { + cleanUpAction(); + } + }, null, 100, 3000); + + proc.WaitForExit(); + + // Retrieve the app's exit code + var exitCode = proc.ExitCode; + if (exitCode != 0) { + request.WriteError(ErrorCategory.InvalidOperation, fastPath, Resources.Messages.InstallFailed, package.Name, proc.StandardError.ReadToEnd()); + request.CompleteProgress(tracker.ProgressID, false); + return false; + } + else { + request.CompleteProgress(tracker.ProgressID, true); + request.YieldFromSwidtag(package, fastPath); + request.Verbose(Resources.Messages.SuccessfullyInstalled, package.Name); + } + cleanUpAction(); + } + return true; + } + else + { + request.Error(ErrorCategory.InvalidOperation, Resources.Messages.FailedToDownload, Constants.ProviderName, package.Source, exePackage); + } + + return false; + } + + internal static void GetInstalledExePackages(PackageJson package, string requiredVersion, string minimumVersion, string maximumVersion, Request request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + request.Debug("Calling '{0}::GetInstalledPackages' '{1}','{2}','{3}','{4}'", Constants.ProviderName, package.Name, requiredVersion, minimumVersion, maximumVersion); + +#if !CORECLR + if (Environment.Is64BitOperatingSystem) { + using (var hklm64 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64).OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey)) { +#else + if (System.Runtime.InteropServices.RuntimeInformation.OSArchitecture == System.Runtime.InteropServices.Architecture.X64) { + using (var hklm64 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64).OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall")) { +#endif + if (!YieldPackages("hklm64", hklm64, package.Name, package.DisplayName, requiredVersion, minimumVersion, maximumVersion, package, request)) { + return; + } + } + + using (var hkcu64 = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64).OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", false)) { + if (!YieldPackages("hkcu64", hkcu64, package.Name, package.DisplayName, requiredVersion, minimumVersion, maximumVersion, package, request)) { + return; + } + } + } + + using (var hklm32 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", false)) { + if (!YieldPackages("hklm32", hklm32, package.Name, package.DisplayName, requiredVersion, minimumVersion, maximumVersion, package, request)) { + return; + } + } + + using (var hkcu32 = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry32).OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall", false)) { + if (!YieldPackages("hkcu32", hkcu32, package.Name, package.DisplayName, requiredVersion, minimumVersion, maximumVersion, package, request)) { + } + } + } + + /// + /// True if matched + /// + /// The package name from user's input + /// The package name found from the system + /// + private static bool IsMatch(string name, string productName) + { + if ((name == null) || (productName == null)) + { + return false; + } + + if (!string.IsNullOrWhiteSpace(name)) + { + if (WildcardPattern.ContainsWildcardCharacters(name)) + { + var wildcardPattern = new WildcardPattern(name, PackageSourceListRequest.WildcardOptions); + return wildcardPattern.IsMatch(productName); + } + else + { + // exactly match with the -name + return (productName.EqualsIgnoreCase(name)); + } + } + else + { + // Considered as matched if name is not specified + return true; + } + } + + private static bool YieldPackages(string hive, RegistryKey regkey, string name, string displayname, string requiredVersion, string minimumVersion, string maximumVersion, PackageJson package, Request request) + { + //TODO make it wildcard match, follow the fastfrence format, get-package git no reults, get-package git* + + if (regkey != null) + { + var includeSystemComponent = request.GetOptionValue("IncludeSystemComponent").IsTrue(); + + foreach (var key in regkey.GetSubKeyNames()) + { + var subkey = regkey.OpenSubKey(key); + if (subkey != null) + { + var properties = subkey.GetValueNames().ToDictionaryNicely(each => each.ToString(), each => (subkey.GetValue(each) ?? string.Empty).ToString(), StringComparer.OrdinalIgnoreCase); + + //if (!includeWindowsInstaller && properties.ContainsKey("WindowsInstaller") && properties["WindowsInstaller"] == "1") + //{ + // continue; + //} + + if (!includeSystemComponent && properties.ContainsKey("SystemComponent") && properties["SystemComponent"] == "1") + { + continue; + } + + var productName = ""; + + if (!properties.TryGetValue("DisplayName", out productName)) + { + // no product name? + continue; + } + + + if (IsMatch(name, productName) || IsMatch(displayname, productName)) + { + var productVersion = properties.Get("DisplayVersion") ?? ""; + var publisher = properties.Get("Publisher") ?? ""; + var uninstallString = properties.Get("QuietUninstallString") ?? properties.Get("UninstallString") ?? ""; + var comments = properties.Get("Comments") ?? ""; + + var fp = hive + @"\" + subkey; + + if (!string.IsNullOrEmpty(requiredVersion)) + { + if (new SemanticVersion(requiredVersion) != new SemanticVersion(productVersion)) + { + continue; + } + } + else { + if (!string.IsNullOrEmpty(minimumVersion) && new SemanticVersion(minimumVersion) > new SemanticVersion(productVersion)) + { + continue; + } + if (!string.IsNullOrEmpty(maximumVersion) && new SemanticVersion(maximumVersion) < new SemanticVersion(productVersion)) + { + continue; + } + } + + fp = PackageSourceListRequest.MakeFastPathComplex(package.Destination ?? "", package.Name, package.DisplayName, productVersion, fp); + + var source = properties.Get("InstallLocation") ?? ""; + //we use name here because find-package uses name (not displayname) in the PSL.json, + + if (request.YieldSoftwareIdentity(fp, name, productVersion, "unknown", comments, source, name, "", "") != null) + { + if (properties.Keys.Where(each => !string.IsNullOrWhiteSpace(each)).Any(k => request.AddMetadata(fp, k.MakeSafeFileName(), properties[k]) == null)) + { + return false; + } + } + } + } + } + } + return true; + } + private static bool YieldPackage(string path, string searchKey, Dictionary properties, Request request) + { + var productName = properties.Get("DisplayName") ?? ""; + var productVersion = properties.Get("DisplayVersion") ?? ""; + var publisher = properties.Get("Publisher") ?? ""; + var uninstallString = properties.Get("QuietUninstallString") ?? properties.Get("UninstallString") ?? ""; + var comments = properties.Get("Comments") ?? ""; + var source = properties.Get("InstallLocation") ?? ""; + + + if (request.YieldSoftwareIdentity(path, productName, productVersion, "unknown", comments, source, searchKey, "", "") != null) + { + if (properties.Keys.Where(each => !string.IsNullOrWhiteSpace(each)).Any(k => request.AddMetadata(path, k.MakeSafeFileName(), properties[k]) == null)) + { + return false; + } + } + return true; + } + internal static void UninstallExePackage(string fastPath, PackageSourceListRequest request) + { + if (string.IsNullOrWhiteSpace(fastPath)) { + return; + } + if (request == null) { + throw new ArgumentNullException("request"); + } + + string sourceLocation; + string id; + string displayName; + string version; + string fastPackageReference; + + if (!request.TryParseFastPathComplex(fastPath: fastPath, regex: PackageSourceListRequest.RegexFastPathComplex, location: out sourceLocation, id: out id, displayname: out displayName, version: out version, fastpath: out fastPackageReference)) + { + request.WriteError(ErrorCategory.InvalidData, fastPath, Resources.Messages.FailedToGetPackageObject, Constants.ProviderName, fastPath); + return; + } + + var ver = (new SemanticVersion(version)).ToString(); + var package = request.GetPackage(id, ver) ?? request.GetPackage(displayName, ver); + + request.Debug("Calling '{0}::UninstallPackage' '{1}'", Constants.ProviderName, fastPackageReference); + + var path = fastPackageReference.Split(new[] { '\\' }, 3); + var uninstallCommand = string.Empty; + Dictionary properties = null; + if (path.Length == 3) + { + switch (path[0].ToLowerInvariant()) + { + case "hklm64": +#if !CORECLR + using (var product = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64).OpenSubKey(path[2], RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey)) +#else + using (var product = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64).OpenSubKey(path[2])) +#endif + { + if (product == null) + { + return; + } + properties = product.GetValueNames().ToDictionaryNicely(each => each.ToString(), each => (product.GetValue(each) ?? string.Empty).ToString(), StringComparer.OrdinalIgnoreCase); + uninstallCommand = properties.Get("QuietUninstallString") ?? properties.Get("UninstallString") ?? ""; + } + break; + case "hkcu64": +#if !CORECLR + using (var product = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64).OpenSubKey(path[2], RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey)) +#else + using (var product = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64).OpenSubKey(path[2])) +#endif + { + if (product == null) + { + return; + } + properties = product.GetValueNames().ToDictionaryNicely(each => each.ToString(), each => (product.GetValue(each) ?? string.Empty).ToString(), StringComparer.OrdinalIgnoreCase); + uninstallCommand = properties.Get("QuietUninstallString") ?? properties.Get("UninstallString") ?? ""; + } + break; + case "hklm32": +#if !CORECLR + using (var product = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(path[2], RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey)) +#else + using (var product = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32).OpenSubKey(path[2])) +#endif + { + if (product == null) + { + return; + } + properties = product.GetValueNames().ToDictionaryNicely(each => each.ToString(), each => (product.GetValue(each) ?? string.Empty).ToString(), StringComparer.OrdinalIgnoreCase); + uninstallCommand = properties.Get("QuietUninstallString") ?? properties.Get("UninstallString") ?? ""; + } + break; + case "hkcu32": +#if !CORECLR + using (var product = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry32).OpenSubKey(path[2], RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey)) +#else + using (var product = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry32).OpenSubKey(path[2])) +#endif + { + if (product == null) + { + return; + } + properties = product.GetValueNames().ToDictionaryNicely(each => each.ToString(), each => (product.GetValue(each) ?? string.Empty).ToString(), StringComparer.OrdinalIgnoreCase); + uninstallCommand = properties.Get("QuietUninstallString") ?? properties.Get("UninstallString") ?? ""; + } + break; + } + + if (properties == null) + { + return; + } + + var result = false; + if (!string.IsNullOrWhiteSpace(uninstallCommand)) + { + do + { + if (File.Exists(uninstallCommand)) + { + result = ExecuteUninstallCommand(fastPackageReference, request, uninstallCommand, package.UnInstallAdditionalArguments); + break; + } + + // not a single file. + // check if it's just quoted. + var c = uninstallCommand.Trim('\"'); + if (File.Exists(c)) + { + result = ExecuteUninstallCommand(fastPackageReference, request, c, package.UnInstallAdditionalArguments); + break; + } + + if (uninstallCommand[0] == '"') + { + var p = uninstallCommand.IndexOf('"', 1); + if (p > 0) + { + var file = uninstallCommand.Substring(1, p - 1); + var args = uninstallCommand.Substring(p + 1); + if (File.Exists(file)) + { + args = string.Join(" ", args, package.UnInstallAdditionalArguments); + result = ExecuteUninstallCommand(fastPackageReference, request, file, args); + } + } + } + else { + var p = uninstallCommand.IndexOf(' '); + if (p > 0) + { + var file = uninstallCommand.Substring(0, p); + var args = uninstallCommand.Substring(p + 1); + if (File.Exists(file)) + { + args = string.Join(" ", args, package.UnInstallAdditionalArguments); + result = ExecuteUninstallCommand(fastPackageReference, request, file, args); + continue; + } + + var s = 0; + do + { + s = uninstallCommand.IndexOf(' ', s + 1); + if (s == -1) + { + break; + } + file = uninstallCommand.Substring(0, s); + if (File.Exists(file)) + { + args = uninstallCommand.Substring(s + 1); + args = string.Join(" ", args, package.UnInstallAdditionalArguments); + result = ExecuteUninstallCommand(fastPackageReference, request, file, args); + break; + } + } while (s > -1); + + if (s == -1) + { + // never found a way to parse the command :( + request.WriteError(Internal.ErrorCategory.InvalidOperation, "DisplayName", properties["DisplayName"], Constants.Messages.UnableToUninstallPackage); + return; + } + } + } + } while (false); + + + if (result) + { + YieldPackage(fastPackageReference, fastPackageReference, properties, request); + return; + } + } + request.WriteError(Internal.ErrorCategory.InvalidOperation, "DisplayName", properties["DisplayName"], Constants.Messages.UnableToUninstallPackage); + } + } + + private static bool ExecuteUninstallCommand(string fastPackageReference, Request request, string file, string args) + { + Timer timer = null; + object timerLock = new object(); + bool cleanUp = false; + + ProgressTracker tracker = new ProgressTracker(request.StartProgress(0, Resources.Messages.Uninstalling)); + double percent = tracker.StartPercent; + + Action cleanUpAction = () => { + lock (timerLock) + { + // check whether clean up is already done before or not + if (!cleanUp) + { + try + { + if (timer != null) + { + // stop timer + timer.Change(Timeout.Infinite, Timeout.Infinite); + timer.Dispose(); + timer = null; + } + } + catch + { + } + + cleanUp = true; + } + } + }; + + var start = new ProcessStartInfo + { + FileName = file, + Arguments = args, + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + //LoadUserProfile = true, + }; + + + using (var proc = Process.Start(start)) + { + // percent between startProgress and endProgress + var progressPercent = tracker.ConvertPercentToProgress(percent += 0.01); + + request.Progress(tracker.ProgressID, (int)progressPercent, Resources.Messages.RunningCommand, file); + + if (proc == null) + { + return false; + } + + timer = new Timer(_ => { + percent += 0.025; + + if (progressPercent < 90) + { + request.Progress(tracker.ProgressID, (int) progressPercent, Resources.Messages.RunningCommand, file); + } + if (request.IsCanceled) + { + cleanUpAction(); + } + }, null, 0, 3000); + + + proc.WaitForExit(); + if (proc.ExitCode != 0) + { + request.Error(ErrorCategory.InvalidOperation, fastPackageReference, Resources.Messages.UninstallFailed, file, proc.StandardError.ReadToEnd()); + request.CompleteProgress(tracker.ProgressID, false); + + return false; + } + request.CompleteProgress(tracker.ProgressID, true); + return true; + } + } + + internal static void DownloadExePackage(string fastPath, string location, PackageSourceListRequest request) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + // TODO do we need to support save-package for executable packages? + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/NupkgInstaller.cs b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/NupkgInstaller.cs new file mode 100644 index 000000000..3e8bedeb6 --- /dev/null +++ b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/NupkgInstaller.cs @@ -0,0 +1,235 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +namespace Microsoft.PackageManagement.PackageSourceListProvider +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Management.Automation; + using Microsoft.PackageManagement.Internal.Api; + using Microsoft.PackageManagement.Internal.Utility.Plugin; + using Microsoft.PackageManagement.Packaging; + using Microsoft.PackageManagement.Provider.Utility; + using ErrorCategory = PackageManagement.Internal.ErrorCategory; + using System.Globalization; + + internal static class NupkgInstaller { + + internal static void GeInstalledNuGetPackages(PackageJson package, string requiredVersion, string minimumVersion, string maximumVersion, Dictionary fastPackReftable, PackageSourceListRequest request) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, Constants.ProviderName, string.Format(CultureInfo.InvariantCulture, "GeInstalledNuGetPackages' - name='{0}', requiredVersion='{1}',minimumVersion='{2}', maximumVersion='{3}'", package.Name, requiredVersion, minimumVersion, maximumVersion)); + + //clone the request + //nuget provider may not know the location of the package gets installed. + //we need to pass around the destination path to nuget provider + var installedRequest = PackageSourceListRequest.ExtendRequest( + new Dictionary { + {"Destination", new[] { package.Destination ?? ""}} + }, null, package.IsTrustedSource, request); + + var provider = PackageSourceListRequest.FindProvider(request, package.Type, request); + if (provider != null) + { + //calling NuGet provider + var packagesInstalled = provider.GetInstalledPackages(package.Name, requiredVersion, minimumVersion, maximumVersion, installedRequest); + if (packagesInstalled != null) + { + foreach (var i in packagesInstalled) + { + request.Debug("Found an installed package '{0}.{1} from {2}' ", i.Name, i.Version, i.Source); + var info = PackageSourceListRequest.MakeFastPathComplex(i.Source, i.Name, "", i.Version, ""); + + fastPackReftable.AddOrSet(info, i); + + // make it semver because in find-package we use semver + var version = i.Version.CompareVersion(package.Version) ? package.Version : i.Version; + request.YieldSoftwareIdentity(info, i.Name, version, i.VersionScheme, i.Summary, i.Source, i.SearchKey, i.FullPath, i.PackageFilename); + + } + } + } + } + + /// + /// Uninstalls a package + /// + /// package defined in the PackageSourceList + /// + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + /// + internal static void UninstallNuGetPackage(PackageJson package, string fastPackageReference, PackageSourceListRequest request, Dictionary fastPackReftable) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, Constants.ProviderName, string.Format(CultureInfo.InvariantCulture, "UninstallNuGetPackage' - fastReference='{0}'", fastPackageReference)); + + var unInstallRequest = PackageSourceListRequest.ExtendRequest( + new Dictionary + { + {"Destination", new[] {package.Destination ?? ""}} + }, null, package.IsTrustedSource, request); + + var provider = PackageSourceListRequest.FindProvider(request, package.Type, request, true); + if (provider != null) + { + request.Debug("{0}: Using the provider '{1} to uninstall the package '{2}'", Constants.ProviderName, provider.Name, package.Name); + + if (!fastPackReftable.ContainsKey(fastPackageReference)) + { + request.WriteError(ErrorCategory.InvalidData, fastPackageReference, Resources.Messages.FailedToGetPackageObject, Constants.ProviderName, fastPackageReference); + return; + } + + var p = fastPackReftable[fastPackageReference]; + + //calling NuGet for uninstall + var installing = provider.UninstallPackage(p, unInstallRequest); + + foreach (var i in installing) + { + request.YieldSoftwareIdentity(i.FastPackageReference, i.Name, i.Version, i.VersionScheme, i.Summary, i.Source, i.SearchKey, i.FullPath, i.PackageFilename); + + if (request.IsCanceled) + { + installing.Cancel(); + } + } + } + } + + internal static void DownloadNuGetPackage(string fastPath, string location, PackageSourceListRequest request) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, Constants.ProviderName, + string.Format(CultureInfo.InvariantCulture, "DownloadNuGetPackage' - fastReference='{0}', location='{1}'", fastPath, location)); + + var package = request.GetPackageByFastPath(fastPath); + if (package == null) + { + request.WriteError(ErrorCategory.InvalidData, fastPath, Resources.Messages.FailedToGetPackageObject, Constants.ProviderName, fastPath); + return; + } + + // let the core figure out how to save this package + var canonicalId = PackageSourceListRequest.CreateCanonicalId(package, Constants.ProviderNames.NuGet); // "nuget:jquery/2.1.0#http://nuget.org/api/v2"; + var pkgs = request.PackageManagementService.FindPackageByCanonicalId(canonicalId, request.As()) + .Where(each => string.IsNullOrWhiteSpace(package.Version) || + (new SemanticVersion(each.Version) == new SemanticVersion(package.Version))).ToArray(); + + switch (pkgs.Length) + { + case 0: + request.Warning(Resources.Messages.CannotFindPackage, Constants.ProviderName, canonicalId); + return; + + case 1: + var provider = request.PackageManagementService.GetAvailableProviders(request, new[] {"NuGet"}).FirstOrDefault(); + if (provider != null) + { + var donwloadrequest = PackageSourceListRequest.ExtendRequest( + new Dictionary + { + {"Destination", new[] {package.Destination ?? ""}} + }, + new[] {package.Source ?? ""}, + package.IsTrustedSource, + request); + + var downloading = provider.DownloadPackage(pkgs[0], location, donwloadrequest); + + foreach (var i in downloading) + { + request.YieldSoftwareIdentity(i.FastPackageReference, i.Name, i.Version, i.VersionScheme, i.Summary, i.Source, i.SearchKey, i.FullPath, i.PackageFilename); + + if (request.IsCanceled) + { + downloading.Cancel(); + } + } + } + break; + default: + request.Warning(Resources.Messages.FoundMorePackages, Constants.ProviderName, pkgs.Length, canonicalId); + return; + } + + } + + internal static void InstallNuGetPackage(PackageJson package, string fastPath, PackageSourceListRequest request) + { + request.Debug(Resources.Messages.DebugInfoCallMethod, Constants.ProviderName, string.Format(CultureInfo.InvariantCulture, "InstallNuGetPackage' - name='{0}', fastPath='{1}'", package.Name, fastPath)); + + var canonicalId = PackageSourceListRequest.CreateCanonicalId(package, Constants.ProviderNames.NuGet); // "nuget:jquery/2.1.0#http://nuget.org/api/v2"; + var pkgs = request.PackageManagementService.FindPackageByCanonicalId(canonicalId, request.As()) + .Where(each => string.IsNullOrWhiteSpace(package.Version) || + (new SemanticVersion(each.Version) == new SemanticVersion(package.Version))).ToArray(); + + switch (pkgs.Length) + { + case 0: + request.Warning(Resources.Messages.CannotFindPackage, Constants.ProviderName, canonicalId); + return; + case 1: + InstallPackageReference(package, request, pkgs); + return; + + default: + request.Warning(Resources.Messages.FoundMorePackages, Constants.ProviderName, pkgs.Length, canonicalId); + return; + } + } + + private static void InstallPackageReference(PackageJson package, PackageSourceListRequest request, SoftwareIdentity[] packages) + { + var installRequest = PackageSourceListRequest.ExtendRequest( + new Dictionary + { + {"Destination", new[] {package.Destination ?? ""}} + }, + new[] { package.Source ?? "" }, + package.IsTrustedSource, + request); + + var provider = PackageSourceListRequest.FindProvider(request, package.Type, installRequest, true); + if (provider == null) + { + return; + } + + var installing = provider.InstallPackage(packages[0], installRequest); + foreach (var i in installing) + { + request.YieldSoftwareIdentity(i.FastPackageReference, i.Name, i.Version, i.VersionScheme, i.Summary, i.Source, i.SearchKey, i.FullPath, i.PackageFilename); + if (request.IsCanceled) + { + installing.Cancel(); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PackageListParser.cs b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PackageListParser.cs new file mode 100644 index 000000000..18e805900 --- /dev/null +++ b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PackageListParser.cs @@ -0,0 +1,442 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using System.Management.Automation; +using Microsoft.PackageManagement.Provider.Utility; +using System.Reflection; +using System.Globalization; + +namespace Microsoft.PackageManagement.PackageSourceListProvider +{ + internal static class JsonParser + { + public static System.Management.Automation.PowerShell _powershell = System.Management.Automation.PowerShell.Create(); + + public static object ConvertObjectToType(object objectToBeConverted, Type typeOfObject) + { + if (typeOfObject.GetTypeInfo().IsPrimitive || typeOfObject == typeof(string)) + { + return objectToBeConverted; + } + + PSObject psObject = objectToBeConverted as PSObject; + + if (psObject == null) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Messages.CannotConvertObject, objectToBeConverted.GetType())); + } + + var returnValue = Activator.CreateInstance(typeOfObject); + + foreach (var property in typeOfObject.GetProperties()) + { + var propertyTypeInfo = property.PropertyType.GetTypeInfo(); + + var matchingProperty = psObject.Properties.FirstOrDefault(psObjectProperty => psObjectProperty.Name.EqualsIgnoreCase(property.Name)); + + if (matchingProperty != null) + { + // primitive type, just cast + if (propertyTypeInfo.IsPrimitive || property.PropertyType == typeof(string)) + { + property.SetValue(returnValue, matchingProperty.Value); + } + else if (propertyTypeInfo.IsGenericType) + { + // maybe list so check that we have 1 generic arguments + var genericArguments = propertyTypeInfo.GetGenericArguments(); + + if (genericArguments.Count() == 1) + { + Type genericArgumentType = genericArguments[0]; + Type genericListType = typeof(List<>).MakeGenericType(genericArgumentType); + + // it's a list so check that the value we got is an array + if (matchingProperty.Value.GetType().GetTypeInfo().IsArray) + { + var newList = Activator.CreateInstance(genericListType); + + var objectList = matchingProperty.Value as object[]; + + var addMethod = genericListType.GetMethod("Add"); + + // populate the array + foreach (var entry in objectList) + { + addMethod.Invoke(newList, new object[] { ConvertObjectToType(entry, genericArgumentType) }); + } + + property.SetValue(returnValue, newList); + } + } + else + { + throw new InvalidDataException(string.Format(CultureInfo.CurrentCulture, Resources.Messages.CannotConvertGenericTypes)); + } + } + else if (propertyTypeInfo.IsClass && matchingProperty.Value is PSObject) + { + // create class using this same function and assign that to the property + property.SetValue(returnValue, ConvertObjectToType(matchingProperty.Value as PSObject, property.PropertyType)); + } + } + + } + + return returnValue; + } + + public static Dictionary> ReadPackageSpec(PackageSource packageSource, string fileToBeProcessed) + { + string packageSpecPath = string.IsNullOrWhiteSpace(fileToBeProcessed) ? packageSource.Location : fileToBeProcessed; + + var jsonFileContent = File.ReadAllText(packageSpecPath); + + PSObject result; + + lock (_powershell) + { + _powershell.Commands.Clear(); + _powershell.AddCommand("ConvertFrom-Json").AddParameter("InputObject", jsonFileContent); + result = _powershell.Invoke().FirstOrDefault(); + } + + var packageListEntries = PopulatePackageSourceList(result, packageSource, null); + + //process dependencies + return ProcessDependencies(packageListEntries); + } + + public static Dictionary> ProcessDependencies(Dictionary> packages) + { + if (packages == null) + { + return packages; + } + + List dependencyList = new List(); + + var PackagesWithDependencies = packages.Values.SelectMany(each => each).Where(item => item.Dependencies != null); + + foreach (var package in PackagesWithDependencies) + { + dependencyList.Clear(); + + foreach (var dep in package.Dependencies) + { + //TODO check name and version + if (packages.ContainsKey(dep.Name)) + { + var depObject = packages[dep.Name].Where(each => (each.Version == dep.Version) && !each.IsCommonDefinition).FirstOrDefault(); + if (depObject != null) + { + dependencyList.Add(depObject); + } + } + else + { + throw new FileFormatException(string.Format("'{0}' is not referencd but not defined in the file '{1}'", dep.Name, package.FilePath)); + } + + } + package.DependencyObjects = dependencyList; + + } + return packages; + } + + public static Dictionary> PopulatePackageSourceList(PSObject psObjectEntries, PackageSource packageSource, string key) + { + var packageSpecPath = packageSource.Location; + + //read access only, and allow others to read at the same time + Dictionary> packages = new Dictionary>(StringComparer.OrdinalIgnoreCase); + ICollection list = new List(); + + foreach (var package in psObjectEntries.Properties) + { + // if this is an array + if (package.Value.GetType().GetTypeInfo().IsArray) + { + var result = PopulateFromArray(package.Value as object[], packageSpecPath, key); + + // check that we got a valid array of entries + if (result != null && result.Count > 0) + { + var common = result.Select(each => each.Common).Where(p => p.Name != null).FirstOrDefault(); + + foreach (var item in result) + { + if (item.Common != null) + { + item.IsCommonDefinition = true; + } + else + { + item.Common = common; + item.FilePath = packageSpecPath; + } + + item.PackageSource = packageSource; + } + + packages.Add(package.Name, result); + } + } + else if (package.Value is PSObject) + { + var convertedPackage = ConvertObjectToType(package.Value, typeof(PackageJson)) as PackageJson; + + if (convertedPackage != null) + { + if (convertedPackage.Common != null) + { + convertedPackage.IsCommonDefinition = true; + } + else + { + convertedPackage.Common = new CommonDefinition(); + } + + convertedPackage.FilePath = packageSpecPath; + convertedPackage.PackageSource = packageSource; + packages.Add(package.Name, new List() { convertedPackage }); + } + } + else + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, Resources.Messages.InvalidPackageListFormat, packageSpecPath)); + } + } + + return packages; + } + + private static List PopulateFromArray(object[] listOfPackages, string packageSpecPath, string key) + { + List result = new List(); + + if (listOfPackages == null) + { + throw new ArgumentNullException(nameof(listOfPackages)); + } + + foreach (var package in listOfPackages) + { + var packageEntry = ConvertObjectToType(package, typeof(PackageJson)) as PackageJson; + + if (packageEntry != null) + { + packageEntry.FilePath = packageSpecPath; + result.Add(packageEntry); + } + } + + return result; + } + } + + + public class OSRequirement + { + public List architecture { get; set; } + public string minimunVersion { get; set; } + public List installationOption { get; set; } + } + + public class CommonDefinition + { + public string Name { get; set; } + public string DisplayName { get; set; } + + public string InstallArguments { get; set; } + public string UnInstallAdditionalArguments { get; set; } + public bool IsPackageProvider { get; set; } + public string Type { get; set; } + + public string Summary { get; set; } + public string License { get; set; } + public string Author { get; set; } + public bool IsTrustedSource { get; set; } + public OSRequirement OS { get; set; } + public string Destination { get; set; } + public List Dependencies { get; set; } + } + + public class Dependencies + { + public string Name { get; set; } + public string DisplayName { get; set; } + public string Version { get; set; } + } + + public class PackageJson + { + private string _license; + private string _name; + private string _displayName; + private string _summary; + private string _author; + private string _destination; + private List _dependencies; + private List _dependencyObjects; + private OSRequirement _osRequirement; + private string _type; + private string _isTrustedSource; + private bool _isPackageProvider; + private string _installArguments; + private string _unInstallAdditionalArguments; + + public CommonDefinition Common { get; set; } + public string Version { get; set; } + + public SemanticVersion SemVer + { + get + { + if (Version != null) + { + return new SemanticVersion(Version); + } + return null; + } + } + public string Source { get; set; } + + public string Name + { + get + { + return _name ?? (Common != null ? Common.Name : null); + } + + set { _name = value; } + } + + + + public string DisplayName + { + get + { + return _displayName ?? (Common != null ? Common.DisplayName : null); + } + + set { _displayName = value; } + } + + public string InstallArguments + { + get + { + return _installArguments ?? (Common != null ? Common.InstallArguments : null); + } + + set { _installArguments = value; } + } + + public string UnInstallAdditionalArguments + { + get + { + return _unInstallAdditionalArguments ?? (Common != null ? Common.UnInstallAdditionalArguments : null); + } + + set { _unInstallAdditionalArguments = value; } + } + + public bool IsPackageProvider + { + // either the Common or Package definition has the IsPackageProvider property defined? + get { return _isPackageProvider || (Common != null && Common.IsPackageProvider); } + + set { _isPackageProvider = value; } + } + + + public string Type + { + get + { + return _type ?? (Common != null ? Common.Type : null); + } + set { _type = value; } + } + + public string Summary + { + get + { + return _summary ?? (Common != null ? Common.Summary : null); + } + set { _summary = value; } + } + public string License { + get + { + return _license ?? (Common != null ? Common.License : null); + } + set { _license = value; } + } + public string Author + { + get + { + return _author ?? (Common != null ? Common.Author : null); + } + set { _author = value; } + } + public string Destination + { + get + { + return _destination ?? (Common != null ? Common.Destination : null); + } + set { _destination = value; } + } + + public bool IsTrustedSource + { + get + { + string istrusted = _isTrustedSource ?? (Common != null ? Common.IsTrustedSource.ToString().ToLowerInvariant() : null); + return istrusted.EqualsIgnoreCase("true"); + } + set { _isTrustedSource = value.ToString().ToLowerInvariant(); } + } + + internal PackageSource PackageSource { get; set; } + + public List Dependencies + { + get + { + return _dependencies ?? (Common != null ? Common.Dependencies : null); + } + set { _dependencies = value; } + } + + public OSRequirement OS + { + get + { + return _osRequirement ?? (Common != null ? Common.OS : null); + } + set { _osRequirement = value; } + } + public bool IsCommonDefinition { get; set; } + + public string FilePath { get; set; } + public string VersionScheme { get; set; } + + public List DependencyObjects + { + get + { + return _dependencyObjects; + } + set { _dependencyObjects = value; } + } + } +} diff --git a/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PackageListProvider.cs b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PackageListProvider.cs new file mode 100644 index 000000000..a2de03128 --- /dev/null +++ b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PackageListProvider.cs @@ -0,0 +1,871 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.PackageSourceListProvider +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Management.Automation; + using Microsoft.PackageManagement.Provider.Utility; + using PackageManagement.Packaging; + using ErrorCategory = PackageManagement.Internal.ErrorCategory; + + public class PackageSourceListProvider { + + private static readonly IEqualityComparer PackageEqualityComparer = new Microsoft.PackageManagement.Provider.Utility.EqualityComparer( + (x, y) => x.Name.EqualsIgnoreCase(y.Name) && x.Version.EqualsIgnoreCase(y.Version), (x) => (x.Name + x.Version).GetHashCode()); + + private readonly Dictionary _fastPackReftable = new Dictionary(StringComparer.OrdinalIgnoreCase); + private const string PackageManagement = "PackageManagement"; + private const string RequiredPackageManagementVersion = "1.0.0.1"; + private static bool _doesPackageManagementVersionMatch = false; + private static Version _currentPackageManagementVersion; + + /// + /// The features that this package supports. + /// + internal static Dictionary Features = new Dictionary { + + //Required by PowerShellGet provider + {Constants.Features.SupportsPowerShellModules, Constants.FeaturePresent}, + + // specify the extensions that your provider uses for its package files (if you have any) + {Constants.Features.SupportedExtensions, new[] {"nupkg", "msi", "WSA", "zip", "exe"}}, + + // you can list the URL schemes that you support searching for packages with + {Constants.Features.SupportedSchemes, new[] {"http", "https", "file", "ftp"}}, + + // Add the minimum module version of PackageManagement + {Constants.Features.PackageManagementMinimumVersion, new[] {RequiredPackageManagementVersion}}, + + // you can list the magic signatures (bytes at the beginning of a file) that we can use + // to peek and see if a given file is yours. + {Constants.Features.MagicSignatures, new[] {Constants.Signatures.Zip}}, + + }; + + + /// + /// Returns the name of the Provider. + /// + /// + /// the name of the package provider + public string PackageProviderName + { + get + { + return Constants.ProviderName; + } + } + + + /// + /// Returns the version of the Provider. + /// + /// The version of this provider + public string ProviderVersion + { + get + { + return Constants.ProviderVersion; + } + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Plugin requirement.")] + public void InitializeProvider(PackageSourceListRequest request) + { + request.Debug("Initialize PackageSourceListProvider"); + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Plugin requirement.")] + public void GetFeatures(PackageSourceListRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + request.Debug("Calling ''{0}'::{1}'", Constants.ProviderName, "GetFeatures"); + + request.Debug("Calling 'PackageSourceListProvider::GetFeatures'"); + foreach (var feature in Features) { + request.Yield(feature); + } + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Plugin requirement.")] + public void GetDynamicOptions(string category, PackageSourceListRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + request.Debug("Calling ''{0}'::{1} ({2})'", Constants.ProviderName, "GetDynamicOptions", category); + + switch ((category ?? string.Empty).ToLowerInvariant()) { + case "package": + break; + + case "source": + break; + + case "install": + request.YieldDynamicOption("Scope", "String", false, new[] {"CurrentUser", "AllUsers"}); + break; + } + } + + public void ResolvePackageSources(PackageSourceListRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + if (!ValidatePackageManagementVersion(request)) + { + return; + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, "ResolvePackageSources"); + + var selectedSources = request.SelectedSources; + + try { + foreach (var source in selectedSources) { + request.Debug(Resources.Messages.YieldingPackageSource, PackageProviderName, source); + request.YieldPackageSource(source.Name, source.Location, source.Trusted, source.IsRegistered, source.IsValidated); + } + } catch (Exception e) { + e.Dump(request); + } + + request.Debug(Resources.Messages.DebugInfoReturnCall, PackageProviderName, "ResolvePackageSources"); + } + + /// + /// This is called when the user is adding (or updating) a package source + /// + /// If this PROVIDER doesn't support user-defined package sources, remove this method. + /// + /// The name of the package source. If this parameter is null or empty the PROVIDER should use the location as the name (if the PROVIDER actually stores names of package sources) + /// The location (ie, directory, URL, etc) of the package source. If this is null or empty, the PROVIDER should use the name as the location (if valid) + /// A boolean indicating that the user trusts this package source. Packages returned from this source should be marked as 'trusted' + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + public void AddPackageSource(string name, string location, bool trusted, PackageSourceListRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + if (!ValidatePackageManagementVersion(request)) + { + return; + } + + try { + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, "AddPackageSource - ProviderName = '{0}', name='{1}', location='{2}', trusted='{3}'", PackageProviderName, name, location, trusted); + + // Error out if a user does not provide package source Name + if (string.IsNullOrWhiteSpace(name)) { + request.WriteError(ErrorCategory.InvalidArgument, Constants.Parameters.Name, Constants.Messages.MissingRequiredParameter, Constants.Parameters.Name); + return; + } + + if (string.IsNullOrWhiteSpace(location)) { + request.WriteError(ErrorCategory.InvalidArgument, Constants.Parameters.Location, Constants.Messages.MissingRequiredParameter, Constants.Parameters.Location); + return; + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, "GetOptionValue"); + // if this is supposed to be an update, there will be a dynamic parameter set for IsUpdatePackageSource + var isUpdate = request.GetOptionValue(Constants.Parameters.IsUpdate).IsTrue(); + + request.Debug(Resources.Messages.VariableCheck, "IsUpdate", isUpdate); + + // if your source supports credentials you get get them too: + // string username =request.Username; + // SecureString password = request.Password; + // feel free to send back an error here if your provider requires credentials for package sources. + + // check first that we're not clobbering an existing source, unless this is an update + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, string.Format(CultureInfo.InvariantCulture, "FindRegisteredSource -name'{0}'", name)); + + var src = request.FindRegisteredSource(name); + + if (src != null && !isUpdate) { + // tell the user that there's one here already + request.WriteError(ErrorCategory.InvalidArgument, name, Constants.Messages.PackageSourceExists, name); + return; + } + + // conversely, if it didn't find one, and it is an update, that's bad too: + if (src == null && isUpdate) { + // you can't find that package source? Tell that to the user + request.WriteError(ErrorCategory.ObjectNotFound, name, Constants.Messages.UnableToResolveSource, Constants.ProviderName, name); + return; + } + + // ok, we know that we're ok to save this source + // next we check if the location is valid (if we support that kind of thing) + + var validated = false; + + if (!request.SkipValidate.Value) { + // the user has not opted to skip validating the package source location, so check if the source is valid + validated = request.ValidateSourceLocation(location); + + if (!validated) { + request.WriteError(ErrorCategory.InvalidData, name, Constants.Messages.SourceLocationNotValid, location); + return; + } + + request.Verbose(Resources.Messages.SuccessfullyValidated, name); + } + + // it's good to check just before you actaully write something to see if the user has cancelled the operation + if (request.IsCanceled) { + return; + } + + // looking good -- store the package source. + request.AddPackageSource(name, location, trusted, validated); + + // Yield the package source back to the caller. + request.YieldPackageSource(name, location, trusted, true /*since we just registered it*/, validated); + } catch (Exception e) { + e.Dump(request); + } + } + + /// + /// Removes/Unregisters a package source + /// + /// The name or location of a package source to remove. + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + public void RemovePackageSource(string name, PackageSourceListRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + if (!ValidatePackageManagementVersion(request)) + { + return; + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, "RemovePackageSource"); + + var src = request.FindRegisteredSource(name); + if (src == null) { + request.Warning(Constants.Messages.UnableToResolveSource, Constants.ProviderName, name); + return; + } + + request.RemovePackageSource(src.Name); + request.YieldPackageSource(src.Name, src.Location, src.Trusted, false, src.IsValidated); + } + + + /// + /// Searches package sources given name and version information + /// + /// Package information must be returned using request.YieldPackage(...) function. + /// + /// a name or partial name of the package(s) requested + /// A specific version of the package. Null or empty if the user did not specify + /// A minimum version of the package. Null or empty if the user did not specify + /// A maximum version of the package. Null or empty if the user did not specify + /// if this is greater than zero (and the number should have been generated using StartFind(...), the core is calling this multiple times to do a batch search request. The operation can be delayed until CompleteFind(...) is called + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Plugin requirement.")] + public void FindPackage(string name, string requiredVersion, string minimumVersion, string maximumVersion, int id, PackageSourceListRequest request) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + if (!ValidatePackageManagementVersion(request)) + { + return; + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, string.Format(CultureInfo.InvariantCulture, "FindPackage' - name='{0}', requiredVersion='{1}',minimumVersion='{2}', maximumVersion='{3}'", name, requiredVersion, minimumVersion, maximumVersion)); + + // no package name or the name with wildcard + if (string.IsNullOrWhiteSpace(name) || WildcardPattern.ContainsWildcardCharacters(name)) + { + // In the case of the package name is null or contains wildcards, error out if a user puts version info + if (!string.IsNullOrWhiteSpace(requiredVersion) || !string.IsNullOrWhiteSpace(minimumVersion) || !string.IsNullOrWhiteSpace(maximumVersion)) + { + request.Warning(Resources.Messages.WildCardCharsAreNotSupported, name); + return; + } + + var packages = request.GetPackages(name); + if (request.GetOptionValue("AllVersions").IsTrue()) + { + // if version is specified then name can not be empty or with wildcard. in this case the cmdlet has been errored out already. + // here we just return all packages we can find + if (request.FilterOnVersion(packages, requiredVersion, minimumVersion, maximumVersion, minInclusive: true, maxInclusive: true, latest: false).Any(p => !request.YieldFromSwidtag(p, p.Name))) + { + return; + } + return; + } + + //return the latest version + if (packages.GroupBy(p => p.Name) + .Select(each => each.OrderByDescending(pp => pp.Version).FirstOrDefault()).Any( item =>!request.YieldFromSwidtag(item, item.Name))) + { + return; + } + + } else { + + // a user specifies name + // asked for a specific version? + if (!string.IsNullOrWhiteSpace(requiredVersion)) + { + request.YieldFromSwidtag(request.GetPackage(name, requiredVersion), name); + return; + } + + var allVersion = request.GetOptionValue("AllVersions").IsTrue(); + // asked for a version range? + if (!string.IsNullOrWhiteSpace(minimumVersion) || !string.IsNullOrEmpty(maximumVersion) || allVersion) + { + var packages = request.GetPackagesWithinVersionRange(name, minimumVersion, maximumVersion); + if (allVersion) + { + if (packages.Any(p => !request.YieldFromSwidtag(p, name))) + { + return; + } + } + else + { + if (request.YieldFromSwidtag(packages.FirstOrDefault(), name)) + { + return; + } + } + + return; + } + + // just return by name + request.YieldFromSwidtag(request.GetPackage(name), name); + } + } + + /// + /// Returns the packages that are installed + /// + /// the package name to match. Empty or null means match everything + /// + /// the specific version asked for. If this parameter is specified (ie, not null or empty + /// string) then the minimum and maximum values are ignored + /// + /// + /// the minimum version of packages to return . If the requiredVersion parameter + /// is specified (ie, not null or empty string) this should be ignored + /// + /// + /// the maximum version of packages to return . If the requiredVersion parameter + /// is specified (ie, not null or empty string) this should be ignored + /// + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Plugin requirement.")] + public void GetInstalledPackages(string name, string requiredVersion, string minimumVersion, string maximumVersion, PackageSourceListRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + if (!ValidatePackageManagementVersion(request)) + { + return; + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, string.Format(CultureInfo.InvariantCulture, "GetInstalledPackages' - name='{0}', requiredVersion='{1}',minimumVersion='{2}', maximumVersion='{3}'", name, requiredVersion, minimumVersion, maximumVersion)); + + IEnumerable packagesDefinedInJsonSpec; + + if (string.IsNullOrWhiteSpace(name) || WildcardPattern.ContainsWildcardCharacters(name)) { + // In the case of the package name is null or contains wildcards, error out if a user puts version info + if (!string.IsNullOrWhiteSpace(requiredVersion) || !string.IsNullOrWhiteSpace(minimumVersion) || !string.IsNullOrWhiteSpace(maximumVersion)) + { + request.Warning(Resources.Messages.WildCardCharsAreNotSupported, name); + return; + } + packagesDefinedInJsonSpec = request.GetPackages(name).ToArray(); + + } else { + //return all installed + packagesDefinedInJsonSpec = request.GetPackages(name, requiredVersion, minimumVersion, maximumVersion).ToArray(); + } + _fastPackReftable.Clear(); + + if (!packagesDefinedInJsonSpec.Any()) + { + request.Verbose(Resources.Messages.NoPackageFound, Constants.ProviderName); + return; + } + + foreach (var package in packagesDefinedInJsonSpec) { + switch (package.Type.ToLowerInvariant()) { + + case Constants.MediaType.AppxPackage: + //TODO for future + break; + case Constants.MediaType.PsArtifacts: + PowerShellArtifactInstaller.GeInstalledPowershellArtifacts(package, requiredVersion, minimumVersion,maximumVersion, _fastPackReftable, request); + break; + case Constants.MediaType.ExePackage: + //program provider can handle get-package git for git.exe + ExePackageInstaller.GetInstalledExePackages(package, requiredVersion, minimumVersion, minimumVersion, request); + break; + case Constants.MediaType.MsiPackage: + //msi provider can handle get-package node.js for node.js.msi + GetMsiInstalledPackage(name, package, requiredVersion, minimumVersion, maximumVersion, request); + break; + case Constants.MediaType.ZipPackage: + ZipPackageInstaller.GetInstalledZipPackage(package, request); + break; + case Constants.MediaType.NuGetPackage: + NupkgInstaller.GeInstalledNuGetPackages(package, requiredVersion, minimumVersion, maximumVersion, _fastPackReftable, request); + break; + + } //switch + } + } + + private void GetMsiInstalledPackage(string name, PackageJson package, string requiredVersion, string minimumVersion, string maximumVersion, PackageSourceListRequest request) { + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, string.Format(CultureInfo.InvariantCulture, "GetMsiInstalledPackage' - name='{0}', requiredVersion='{1}',minimumVersion='{2}', maximumVersion='{3}'", name, requiredVersion, minimumVersion, maximumVersion)); + + var provider = PackageSourceListRequest.FindProvider(request, package.Type, request); + if (provider != null) { + var packagesInstalled = provider.GetInstalledPackages(package.Name, requiredVersion, minimumVersion, maximumVersion, request); + + if (!packagesInstalled.Any()) { + packagesInstalled = provider.GetInstalledPackages(package.DisplayName, requiredVersion, minimumVersion, maximumVersion, request); + } + foreach (var i in packagesInstalled) + { + request.Debug("found a package '{0}.{1}' installed from '{2}'", i.Name, i.Version, i.Source); + + var info = PackageSourceListRequest.MakeFastPathComplex(i.Source, name, (package.DisplayName?? ""), i.Version, i.FastPackageReference); + + _fastPackReftable.AddOrSet(i.FastPackageReference, i); + + // check if the installed version matches with the one specified in the PSL.json. + // If so, we choose PSL.json. + var version = i.Version.CompareVersion(package.Version) ? package.Version : i.Version; + //we use displayname here because msi provider uses the displayname. + request.YieldSoftwareIdentity(info, package.DisplayName, version, i.VersionScheme, i.Summary, package.Source, i.SearchKey, i.FullPath, i.PackageFilename); + return; + } + } + } + private void UnInstallMsiPackage(PackageSourceListRequest request, string fastPath, PackageJson package) + { + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, string.Format(CultureInfo.InvariantCulture, "UnInstallMsiPackage' - name='{0}', fastPath='{1}'", package.Name, fastPath)); + + string sourceLocation; + string id; + string displayName; + string version; + string fastPackageReference; + + if (!request.TryParseFastPathComplex(fastPath: fastPath, regex: PackageSourceListRequest.RegexFastPathComplex, location: out sourceLocation, id: out id, displayname: out displayName, version: out version, fastpath: out fastPackageReference)) + { + //we don't need to error out even if fastpath is not correct because msi provider is expected to handle the uninstall-package. + request.Verbose(Resources.Messages.UnsupportMSIUninstall, Constants.ProviderName, package.Name); + return; + } + + // Normally uninstall-package will be handled by MSI provider. Here we added a special case for handling uninstall-package nodejs + // which msi provider unable to deal with (node.js only for msi) + if (id != null && id.EqualsIgnoreCase("nodejs")) + { + + var provider = PackageSourceListRequest.FindProvider(request, Constants.ProviderNames.Msi, request); + + if (provider != null) + { + + if (!_fastPackReftable.ContainsKey(fastPackageReference)) + { + request.WriteError(ErrorCategory.InvalidData, fastPackageReference, Resources.Messages.FailedToGetPackageObject, Constants.ProviderName, fastPackageReference); + return; + } + + request.Verbose(Resources.Messages.UninstallingPackage, Constants.ProviderName, package.Name); + + var p = _fastPackReftable[fastPackageReference]; + + var installing = provider.UninstallPackage(p, request); + + foreach (var i in installing) + { + request.YieldSoftwareIdentity(i.FastPackageReference, i.Name, i.Version, i.VersionScheme, + i.Summary, package.Source, i.SearchKey, i.FullPath, i.PackageFilename); + if (request.IsCanceled) + { + installing.Cancel(); + } + return; + } + } + } + else + { + //no-op for uninstalling the msi packages. only install-package nodejs is supported because msi can not handle it + request.Verbose(Resources.Messages.UnsupportMSIUninstall, Constants.ProviderName, package.Name); + return; + } + + } + + + /// + /// Uninstalls a package + /// + /// + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + public void UninstallPackage(string fastPackageReference, PackageSourceListRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + if (!ValidatePackageManagementVersion(request)) + { + return; + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, fastPackageReference); + + var package = request.GetFastReferenceComplex(fastPackageReference); + if (package == null) + { + request.WriteError(ErrorCategory.InvalidData, fastPackageReference, Resources.Messages.FailedToGetPackageObject, Constants.ProviderName, fastPackageReference); + return; + } + + switch (package.Type.ToLowerInvariant()) { + + case Constants.MediaType.AppxPackage: + //TODO for the future + break; + case Constants.MediaType.ExePackage: + ExePackageInstaller.UninstallExePackage(fastPackageReference, request); + break; + case Constants.MediaType.MsiPackage: + + UnInstallMsiPackage(request, fastPackageReference, package); + break; + case Constants.MediaType.ZipPackage: + ZipPackageInstaller.UnInstallZipPackage(request, fastPackageReference); + break; + case Constants.MediaType.NuGetPackage: + NupkgInstaller.UninstallNuGetPackage(package, fastPackageReference, request, _fastPackReftable); + break; + case Constants.MediaType.PsArtifacts: + PowerShellArtifactInstaller.UninstallPowershellArtifacts(package, fastPackageReference, request, _fastPackReftable); + break; + default: + request.WriteError(ErrorCategory.InvalidData, fastPackageReference, Resources.Messages.UnknownMediaType, package.Name, package.Source, package.Type); + break; + } + } + + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Plugin requirement.")] + public void DownloadPackage(string fastPath, string location, PackageSourceListRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + if (!ValidatePackageManagementVersion(request)) + { + return; + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, string.Format(CultureInfo.InvariantCulture, "DownloadPackage' - fastReference='{0}', location='{1}'", fastPath, location)); + + var package = request.GetPackageByFastPath(fastPath); + + if (package == null) + { + request.WriteError(ErrorCategory.InvalidData, fastPath, Resources.Messages.FailedToGetPackageObject, Constants.ProviderName, fastPath); + return; + } + + switch (package.Type.ToLowerInvariant()) { + case Constants.MediaType.MsiPackage: + //No-op as the msi provider does not support save-package + break; + case Constants.MediaType.MsuPackage: + //No-op as the msu provider does not support save-package + break; + case Constants.MediaType.AppxPackage: + //TODO for future whenever needed to support appx packages + break; + case Constants.MediaType.NuGetPackage: + NupkgInstaller.DownloadNuGetPackage(fastPath, location, request); + break; + case Constants.MediaType.ZipPackage: + //TODO + ZipPackageInstaller.DownloadZipPackage(fastPath, location, request); + break; + case Constants.MediaType.ExePackage: + //TODO + ExePackageInstaller.DownloadExePackage(fastPath, location, request); + break; + case Constants.MediaType.PsArtifacts: + //TODO + break; + + default: + request.WriteError(ErrorCategory.InvalidData, fastPath, Resources.Messages.UnknownMediaType, package.Name, package.Source, package.Type); + return; + } + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Plugin requirement.")] + public void InstallPackage(string fastPath, PackageSourceListRequest request) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + if (!ValidatePackageManagementVersion(request)) + { + return; + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, PackageProviderName, string.Format(CultureInfo.InvariantCulture, "InstallPackage' - fastReference='{0}'", fastPath)); + + var package = request.GetPackageByFastPath(fastPath); + if (package == null) + { + request.WriteError(ErrorCategory.InvalidData, fastPath, Resources.Messages.FailedToGetPackageObject, Constants.ProviderName, fastPath); + return; + } + + InstallProviderFromInstaller(package, fastPath, request); + } + + internal static void InstallProviderFromInstaller(PackageJson package, string fastPath, PackageSourceListRequest request) { + + request.Debug(Resources.Messages.DebugInfoCallMethod, Constants.ProviderName, string.Format(CultureInfo.InvariantCulture, "InstallProviderFromInstaller' - name='{0}', fastPath='{1}'", package.Name, fastPath)); + + //install any dependency packages + InstallDependencies(package, request); + + switch (package.Type.ToLowerInvariant()) { + case Constants.MediaType.MsiPackage: + case Constants.MediaType.MsuPackage: + InstallPackageFile(package, fastPath, request); + break; + case Constants.MediaType.AppxPackage: + //TODO for future whenever needed to support appx packages + break; + case Constants.MediaType.NuGetPackage: + NupkgInstaller.InstallNuGetPackage(package, fastPath, request); + break; + case Constants.MediaType.ZipPackage: + ZipPackageInstaller.InstallZipPackage(package, fastPath, request); + break; + case Constants.MediaType.ExePackage: + ExePackageInstaller.InstallExePackage(package, fastPath, request); + break; + case Constants.MediaType.PsArtifacts: + PowerShellArtifactInstaller.InstallPowershellArtifacts(package, fastPath, request); + break; + + default: + request.WriteError(ErrorCategory.InvalidData, fastPath, Resources.Messages.UnknownMediaType, package.Name, package.Source, package.Type); + break; + } + return; + } + + private static void InstallPackageFile(PackageJson package, string fastPath, PackageSourceListRequest request) + { + request.Debug(Resources.Messages.DebugInfoCallMethod, Constants.ProviderName, string.Format(CultureInfo.InvariantCulture, "InstallPackageFile' - name='{0}', fastPath='{1}'", package.Name, fastPath)); + + // get a temp file name, msi or msu + var providerType = package.Type.ToLowerInvariant(); + var destination = Path.ChangeExtension(Path.GetTempFileName(), providerType); + + //download the msi package to the temp file + WebDownloader.DownloadFile(package.Source, destination, request, null); + + if (!File.Exists(destination)) + { + return; + } + // validate the file + if (!WebDownloader.PerformSecurityScan(destination)) + { + //TODO Security Scan work + request.WriteError(ErrorCategory.InvalidOperation, Constants.ProviderName, "The package download from '{0}' failed in the security scan.", package.Source); + return; + } + + + if (!package.IsTrustedSource) + { + if (!request.ShouldContinueWithUntrustedPackageSource(package.Name, package.Source)) + { + request.Warning(Constants.Messages.UserDeclinedUntrustedPackageInstall, package.Name); + return; + } + } + + var installRequest = PackageSourceListRequest.ExtendRequest(new Dictionary + { + {"Source", new[] {package.Source ?? ""}} + }, new[] { package.Source ?? "" }, package.IsTrustedSource, request); + + request.Verbose(Resources.Messages.CallMsiForInstall, package.Name); + if (request.ProviderServices.Install(destination, "", installRequest)) + { + // it installed ok!exit + request.YieldFromSwidtag(package, fastPath); + return; + } + else + { + request.WriteError(ErrorCategory.InvalidOperation, Constants.ProviderName, Resources.Messages.PackageFailedInstall, package.Name); + } + } + + internal static void InstallDependencies(PackageJson packageJson, PackageSourceListRequest request) + { + //TODO dependency chain check + + //let's install dependency first in case it is needed for installing the actual package + if (packageJson.DependencyObjects != null) + { + var dependencies = GetDependencies(packageJson, request); + + if (dependencies != null) + { + foreach (var dep in dependencies.Where(dep => (dep != null) && !dep.IsCommonDefinition)) + { + //dependency source trusty follows its parent + dep.IsTrustedSource |= packageJson.IsTrustedSource; + var id = PackageSourceListRequest.CreateCanonicalId(dep, dep.Name); + InstallProviderFromInstaller(dep, id, request); + } + } + } + } + + private static IEnumerable GetDependencies(PackageJson packageJson, PackageSourceListRequest request) { + + if (packageJson.DependencyObjects == null) { + yield break; + } + + bool force = request.GetOptionValue("Force") != null; + foreach (var dep in packageJson.DependencyObjects.Where(dep => (dep != null) && !dep.IsCommonDefinition)) { + if (!force) { + var provider = PackageSourceListRequest.FindProvider(request, dep.Type, request, true); + + if(provider == null) + { + //FindProvider logged an error already + break; + } + //Check whether the dependency package is installed + var installedPackages = provider.GetInstalledPackages(dep.Name, requiredVersion: null, minimumVersion: dep.Version, maximumVersion: null, requestObject: request); + + if (installedPackages == null || !installedPackages.Any()) { + request.Verbose(Resources.Messages.DependencyNotInstalled, dep.Name); + yield return dep; + } else { + request.Verbose(Resources.Messages.DependencyInstalled, dep.Name); + } + } else { + yield return dep; + } + } + } + + private bool ValidatePackageManagementVersion(PackageSourceListRequest request) + { + var userSpecifiedProvider = request.GetOptionValue("ProviderName") ?? request.GetOptionValue("Provider"); + + if (_currentPackageManagementVersion == null) + { + var moduleInfo = GetModule("PackageManagement"); + + if(moduleInfo == null || !moduleInfo.Any()) + { + request.Verbose(Resources.Messages.CannotFindPackageManagementVersion); + return false; + } + + _currentPackageManagementVersion = moduleInfo.OrderByDescending(each => each.Version).FirstOrDefault().Version; + if (_currentPackageManagementVersion < new Version(RequiredPackageManagementVersion)) + { + _doesPackageManagementVersionMatch = false; + } + else + { + _doesPackageManagementVersionMatch = true; + } + } + + if (!_doesPackageManagementVersionMatch) + { + if (userSpecifiedProvider != null && userSpecifiedProvider.IndexOf(Constants.ProviderName, StringComparison.OrdinalIgnoreCase) > -1) + { + request.WriteError(ErrorCategory.InvalidOperation, Constants.ProviderName, Resources.Messages.MininumVersonCheck, Constants.ProviderName, RequiredPackageManagementVersion, _currentPackageManagementVersion); + } + else + { + request.Verbose(Resources.Messages.MininumVersonCheck, Constants.ProviderName, RequiredPackageManagementVersion, _currentPackageManagementVersion); + } + } + + return _doesPackageManagementVersionMatch; + } + + internal static IEnumerable GetModule(string moduleName) + { + + using (PowerShell powershell = PowerShell.Create()) + { + if (powershell != null) + { + return powershell + .AddCommand("Get-Module") + .AddParameter("Name", moduleName) + .AddParameter("ListAvailable") + .Invoke(); + } + } + + return Enumerable.Empty(); + + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PackageListRequest.cs b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PackageListRequest.cs new file mode 100644 index 000000000..df67630d9 --- /dev/null +++ b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PackageListRequest.cs @@ -0,0 +1,1073 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +namespace Microsoft.PackageManagement.PackageSourceListProvider +{ + using System; + using System.Globalization; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using Microsoft.PackageManagement.Internal.Implementation; + using System.Text.RegularExpressions; + using System.Xml.Linq; + using System.Xml; + using System.Collections.Concurrent; + using System.Net.Http; + using Internal.Utility.Plugin; + using System.Management.Automation; + using ErrorCategory = PackageManagement.Internal.ErrorCategory; + using Microsoft.PackageManagement.Implementation; + using Microsoft.PackageManagement.Internal.Api; + using Microsoft.PackageManagement.Provider.Utility; + + public abstract class PackageSourceListRequest : Request { + + private IEnumerable _packageQuery; + private static IDictionary _registeredPackageSources; + private string _configurationFileLocation; + private XDocument _config; + private string _defaultConfig; + private IEnumerable _packageSources; + private const string _PackageSourceListRequest = "PackageSourceListRequest"; + private HttpClient _httpClient; + //private HttpClient _httpClientWithoutAcceptHeader; + + private static IDictionary _checkedUnregisteredPackageSources = new ConcurrentDictionary(); + private static Dictionary _packageProviders = new Dictionary(); + + private static readonly Regex RegexFastPath = new Regex(@"\$(?[\w,\+,\/,=]*)\\(?[\w,\+,\/,=]*)"); + internal static readonly Regex RegexFastPathComplex = new Regex(@"\$(?[\w,\+,\/,=]*)#(?[\w,\+,\/,=]*)#(?[\w,\+,\/,=]*)#(?[\w,\+,\/,=]*)#(?[\w,\+,\/,=]*)"); + + internal Lazy SkipValidate; + internal readonly Lazy AllVersions; + internal readonly Lazy Headers; + + internal const WildcardOptions WildcardOptions = System.Management.Automation.WildcardOptions.CultureInvariant | System.Management.Automation.WildcardOptions.IgnoreCase; + + internal const string DefaultConfigDefinition = @" + + + + +"; + + /// + /// Ctor required by the PackageManagement Platform + /// + protected PackageSourceListRequest() + { + AllVersions = new Lazy(() => GetOptionValue("AllVersions").IsTrue()); + SkipValidate = new Lazy(() => GetOptionValue("SkipValidate").IsTrue()); + Headers = new Lazy(() => (GetOptionValues("Headers") ?? new string[0]).ToArray()); + } + internal string DefaultConfigLocation + { + get + { +#if CORECLR + return Path.Combine(Environment.GetEnvironmentVariable("ProgramFiles"), "PackageManagement\\ProviderAssemblies\\PSL", "PSL.json"); +#else + return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "PackageManagement\\ProviderAssemblies\\PSL", "PSL.json"); +#endif + } + } + + + + internal string DefaultConfig + { + get + { + if (_defaultConfig == null) + { + _defaultConfig = DefaultConfigDefinition.Replace("##", DefaultConfigLocation); + + } + return _defaultConfig; + } + } + + internal IEnumerable PackageSources + { + get + { + return _packageSources ?? (_packageSources = (Sources ?? new string[0]).ToArray()); + } + } + private IEnumerable Packages + { + get + { + if (_packageQuery == null) + { + try + { + _packageQuery = SelectedSources.AsParallel().WithMergeOptions(ParallelMergeOptions.NotBuffered).SelectMany(source => + (new PackageQuery(source, this)).SingleItemAsEnumerable()).Where(each=>each.IsValid); + } + catch (Exception ex) + { + ex.Dump(this); + } + } + return _packageQuery; + } + } + + internal virtual bool IsValid + { + get + { + return _packageQuery != null; + } + } + + internal IEqualityComparer PackageEqualityComparer { get; private set; } + + /// + /// Supported package source schemes by this provider + /// + internal static IEnumerable SupportedSchemes + { + get + { + return PackageSourceListProvider.Features[Constants.Features.SupportedSchemes]; + } + } + private string GetValue(string name) { + // get the value from the request + return (GetOptionValues(name) ?? Enumerable.Empty()).LastOrDefault(); + } + + internal PackageJson GetPackage(string name) { + return Packages.SelectMany(each => each.Query(name)).OrderByDescending(p => p.Version).FirstOrDefault(); + } + + internal PackageJson GetPackage(string name, string requiredVersion) + { + if (string.IsNullOrWhiteSpace(requiredVersion)) + { + return Packages.SelectMany(each => each.Query(name)).FirstOrDefault(); + } + + return Packages.SelectMany(each => each.Query(name, requiredVersion)).FirstOrDefault(); + } + + internal IEnumerable GetPackagesWithinVersionRange(string name, string minimumversion, string maximumversion) + { + return Packages.SelectMany(each => each.Query(name, minimumversion, maximumversion)) + .OrderByDescending(p => p.Version); + } + + internal IEnumerable GetPackages(string name) + { + var wildcardPattern = new WildcardPattern(name, WildcardOptions); + + return Packages.SelectMany(each => each.Query()).Distinct(PackageEqualityComparer) + .Where(p => !string.IsNullOrWhiteSpace(p.Name) && (string.IsNullOrWhiteSpace(name) || (wildcardPattern.IsMatch(p.Name) || wildcardPattern.IsMatch(p.DisplayName)))); + } + + internal IEnumerable GetPackages(string name, string requiredVersion, string minimumversion, string maximumversion) + { + if (!string.IsNullOrWhiteSpace(requiredVersion)) + { + return Packages.SelectMany(each => each.Query(name, requiredVersion)); + } + + return Packages.SelectMany(each => each.Query(name, minimumversion, maximumversion)) + .OrderByDescending(p => p.Version); + } + + internal static PackageProvider FindProvider(PackageSourceListRequest request, string providerType, IHostApi hostApi, bool logError = false) { + + string providerName = null; + switch (providerType.ToLowerInvariant()) { + case Constants.MediaType.MsiPackage: + providerName = Constants.ProviderNames.Msi; + break; + case Constants.MediaType.MsuPackage: + providerName = Constants.ProviderNames.Msu; + break; + case Constants.MediaType.AppxPackage: + //TODO for future whenever needed to support appx packages + break; + case Constants.MediaType.NuGetPackage: + providerName = Constants.ProviderNames.NuGet; + break; + case Constants.MediaType.ZipPackage: + case Constants.MediaType.ExePackage: + providerName = Constants.ProviderNames.PSL; + break; + case Constants.MediaType.PsArtifacts: + providerName = Constants.ProviderNames.PowerShellGet; + break; + + default: + request.Warning(Resources.Messages.UnsupportedProviderType, providerType); + break; + } + + if (string.IsNullOrWhiteSpace(providerName)) { + return null; + } + + PackageProvider provider; + if (_packageProviders.ContainsKey(providerName)) { + provider = _packageProviders[providerName]; + } else { + provider = request.PackageManagementService.SelectProviders(providerName, request).FirstOrDefault(); + if (provider != null) { + _packageProviders.AddOrSet(providerName, provider); + } + } + + if (provider != null) { + return provider; + } + + request.Verbose(Resources.Messages.ProviderNotFound, providerName); + + if (logError) { + request.Error(ErrorCategory.InvalidOperation, providerName, Resources.Messages.ProviderNotFound, providerName); + } + return null; + } + + internal static string CreateCanonicalId(PackageJson package, string providerName) + { + // example: "nuget:jquery/2.1.0#http://nuget.org/api/v2" + if (package == null || string.IsNullOrEmpty(package.Name) || string.IsNullOrEmpty(providerName)) + { + return null; + } + if (string.IsNullOrWhiteSpace(package.Version) && string.IsNullOrWhiteSpace(package.Source)) + { + return "{0}:{1}".format(CultureInfo.CurrentCulture.TextInfo.ToLower(providerName), package.Name); + } + if (string.IsNullOrWhiteSpace(package.Source)) + { + return "{0}:{1}/{2}".format(CultureInfo.CurrentCulture.TextInfo.ToLower(providerName), package.Name, package.Version); + } + + Uri pkgId; + var source = package.Source; + if (Uri.TryCreate(package.Source, UriKind.Absolute, out pkgId)) + { + source = pkgId.AbsoluteUri; + } + + if (string.IsNullOrWhiteSpace(package.Version)) + { + "{0}:{1}#{2}".format(CultureInfo.CurrentCulture.TextInfo.ToLower(providerName), package.Name, source); + } + + return "{0}:{1}/{2}#{3}".format(CultureInfo.CurrentCulture.TextInfo.ToLower(providerName), package.Name, package.Version, source); + } + + + internal string MakeFastPath(PackageJson package) + { + return string.Format(CultureInfo.InvariantCulture, @"${0}\{1}", package.Name.ToBase64(), package.Version.ToBase64()); + } + private bool TryParseFastPath(string fastPath, Regex regex, out string id, out string version) + { + var match = regex.Match(fastPath); + id = match.Success ? match.Groups["id"].Value.FromBase64() : null; + version = match.Success ? match.Groups["version"].Value.FromBase64() : null; + return match.Success; + } + + /// + /// Get the PackageItem object from the fast path + /// + /// + /// + internal PackageJson GetPackageByFastPath(string fastPath) + { + string id; + string version; + + if (TryParseFastPath(fastPath, RegexFastPath, out id, out version)) + { + return GetPackage(id, version); + } + + return null; + } + + + internal static string MakeFastPathComplex(string source, string name, string displayName, string version, string fastPackageReference) + { + return string.Format(CultureInfo.InvariantCulture, @"${0}#{1}#{2}#{3}#{4}", (source ?? "").ToBase64(), (name ?? "").ToBase64(), (displayName ?? "").ToBase64(), (version ?? "").ToBase64(), (fastPackageReference ?? "").ToBase64()); + } + + internal bool TryParseFastPathComplex(string fastPath, Regex regex, out string location, out string id, out string displayname, out string version, out string fastpath) + { + var match = regex.Match(fastPath); + location = match.Success ? match.Groups["location"].Value.FromBase64() : null; + id = match.Success ? match.Groups["id"].Value.FromBase64() : null; + displayname = match.Success ? match.Groups["displayname"].Value.FromBase64() : null; + version = match.Success ? match.Groups["version"].Value.FromBase64() : null; + fastpath = match.Success ? match.Groups["fastpath"].Value.FromBase64() : null; + + return match.Success; + } + + + internal PackageJson GetFastReferenceComplex(string fastPath) + { + string sourceLocation; + string id; + string displayname; + string version; + string fastreference; + + if (TryParseFastPathComplex(fastPath: fastPath, regex: RegexFastPathComplex, location: out sourceLocation, id: out id, displayname: out displayname, version: out version, fastpath: out fastreference)) + { + var ver = (new SemanticVersion(version)).ToString(); + return GetPackage(id, ver) ?? GetPackage(displayname, ver); + } + + return null; + } + + + internal IEnumerable FilterOnVersion(IEnumerable pkgs, string requiredVersion, string minimumVersion, string maximumVersion, bool minInclusive = true, bool maxInclusive = true, bool latest=true) + { + if (!string.IsNullOrWhiteSpace(requiredVersion)) + { + pkgs = pkgs.Where(each => each.SemVer == new SemanticVersion(requiredVersion)); + } + else + { + if (!string.IsNullOrWhiteSpace(minimumVersion)) + { + // if minInclusive, then use >= else use > + if (minInclusive) + { + pkgs = pkgs.Where(each => each.SemVer >= new SemanticVersion(minimumVersion)); + } + else + { + pkgs = pkgs.Where(each => each.SemVer > new SemanticVersion(minimumVersion)); + } + } + + if (!String.IsNullOrWhiteSpace(maximumVersion)) + { + // if maxInclusive, then use < else use <= + if (maxInclusive) + { + pkgs = pkgs.Where(each => each.SemVer <= new SemanticVersion(maximumVersion)); + } + else + { + pkgs = pkgs.Where(each => each.SemVer < new SemanticVersion(maximumVersion)); + } + } + } + + return latest ? pkgs.OrderByDescending(each => each.Version).FirstOrDefault().SingleItemAsEnumerable() : pkgs; + } + + internal bool YieldFromSwidtag(PackageJson package, string searchKey) + { + if (package == null) + { + return !IsCanceled; + } + + var targetFilename = package.FilePath; + var summary = package.Summary; + + var fastPath = MakeFastPath(package); + + // As 'nodejs' become 'node.js' after the install, GetInstalledPackage() will return node.js. To skip installing node.js + // if already installed, we need to return the node.js i.e., use display name for msi provider. + if (package.Type.EqualsIgnoreCase(Constants.MediaType.MsiPackage)) + { + if (!string.IsNullOrWhiteSpace(package.DisplayName)) + { + package.Name = package.DisplayName; + } + } + + if (YieldSoftwareIdentity(fastPath, package.Name, package.SemVer.ToString(), package.VersionScheme, summary, package.Source, searchKey, null, targetFilename) != null) + { + //this is a trusted source + if (AddMetadata(fastPath, "FromTrustedSource", "true") == null) + { + return !IsCanceled; + } + + if (AddMetadata(fastPath, "Source", package.Source) == null) + { + return !IsCanceled; + } + if (AddMetadata(fastPath, "Type", package.Type) == null) + { + return !IsCanceled; + } + + if (package.Dependencies != null) + { + //iterate thru the dependencies and add them to the software identity. + foreach (var dependency in package.Dependencies) + { + // foreach (var pkg in dependency) + { + AddDependency("PSL", dependency.Name, dependency.Version, null, null); + } + } + } + } + return !IsCanceled; + } + + /// + /// Unregister the package source + /// + /// package source id or name + internal void RemovePackageSource(string id) + { + Debug(Resources.Messages.DebugInfoCallMethod3, Constants.ProviderName, "RemovePackageSource", id); + var config = Config; + if (config == null) + { + return; + } + + try + { + + XElement configuration = config.ElementsNoNamespace("configuration").FirstOrDefault(); + if (configuration == null) + { + return; + } + + XElement packageSources = configuration.ElementsNoNamespace("packageSources").FirstOrDefault(); + if (packageSources == null) + { + return; + } + + var nodes = packageSources.Elements("add"); + if (nodes == null) + { + return; + } + + foreach (XElement node in nodes) + { + + if (node.Attribute("key") != null && String.Equals(node.Attribute("key").Value, id, StringComparison.OrdinalIgnoreCase)) + { + // remove itself + node.Remove(); + Config = config; + Verbose(Resources.Messages.RemovedPackageSource, id); + break; + } + + } + + if (_registeredPackageSources.ContainsKey(id)) + { + _registeredPackageSources.Remove(id); + } + + if (_checkedUnregisteredPackageSources.ContainsKey(id)) + { + _checkedUnregisteredPackageSources.Remove(id); + } + + + //var source = config.SelectNodes("/configuration/packageSources/add").Cast().FirstOrDefault(node => String.Equals(node.Attributes["key"].Value, id, StringComparison.CurrentCultureIgnoreCase)); + + //if (source != null) + //{ + // source.ParentNode.RemoveChild(source); + // Config = config; + // Verbose(Resources.Messages.RemovedPackageSource, id); + //} + } + catch (Exception ex) + { + ex.Dump(this); + } + } + + /// + /// Register the package source + /// + /// package source name + /// package source location + /// is the source trusted + /// need validate before storing the information to config file + internal void AddPackageSource(string name, string location, bool isTrusted, bool isValidated) + { + + Debug(Resources.Messages.DebugInfoCallMethod, Constants.ProviderName, string.Format(CultureInfo.InvariantCulture, "AddPackageSource - name= {0}, location={1}", name, location)); + try + { + // here the source is already validated by the caller + var config = Config; + if (config == null) + { + return; + } + + XElement source = null; + XElement packageSources = null; + // Check whether there is an existing node with the same name + var configuration = config.ElementsNoNamespace("configuration").FirstOrDefault(); + if (configuration != null) + { + packageSources = configuration.ElementsNoNamespace("packageSources").FirstOrDefault(); + if (packageSources != null) + { + source = packageSources.Elements("add").FirstOrDefault(node => + node.Attribute("key") != null && String.Equals(node.Attribute("key").Value, name, StringComparison.OrdinalIgnoreCase)); + } + } + else + { + // create configuration node if it does not exist + configuration = new XElement("configuration"); + // add that to the config + config.Add(configuration); + } + + // There is no existing node with the same name. So we have to create one. + if (source == null) + { + // if packagesources is null we have to create that too + if (packageSources == null) + { + // create packagesources node + packageSources = new XElement("packageSources"); + // add that to the config + configuration.Add(packageSources); + } + + // Create new source + source = new XElement("add"); + // Add that to packagesource + packageSources.Add(source); + } + + // Now set the source node properties + source.SetAttributeValue("key", name); + source.SetAttributeValue("value", location); + if (isValidated) + { + source.SetAttributeValue("validated", true.ToString()); + } + if (isTrusted) + { + source.SetAttributeValue("trusted", true.ToString()); + } + + // Write back to the config file + Config = config; + + // Add or set the source node from the dictionary depends on whether it was there + if (_registeredPackageSources.ContainsKey(name)) + { + var packageSource = _registeredPackageSources[name]; + packageSource.Name = name; + packageSource.Location = location; + packageSource.Trusted = isTrusted; + packageSource.IsRegistered = true; + packageSource.IsValidated = isValidated; + } + else + { + _registeredPackageSources.Add(name, new PackageSource + { + Request = this, + Name = name, + Location = location, + Trusted = isTrusted, + IsRegistered = true, + IsValidated = isValidated, + }); + } + + } + catch (Exception ex) + { + ex.Dump(this); + } + } + + /// + /// Return the package source object + /// + /// The package source name to search for + /// package source object + internal PackageSource FindRegisteredSource(string name) + { + + Debug(Resources.Messages.DebugInfoCallMethod3, Constants.ProviderName, "FindRegisteredSource", name); + var srcs = RegisteredPackageSources; + + if (srcs == null) + { + return null; + } + if (srcs.ContainsKey(name)) + { + return srcs[name]; + } + + var src = srcs.Values.FirstOrDefault(each => LocationCloseEnoughMatch(name, each.Location)); + return src; + } + + private bool LocationCloseEnoughMatch(string givenLocation, string knownLocation) + { + if (givenLocation.Equals(knownLocation, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // make trailing slashes consistent + if (givenLocation.TrimEnd('/').Equals(knownLocation.TrimEnd('/'), StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + // and trailing backslashes + if (givenLocation.TrimEnd('\\').Equals(knownLocation.TrimEnd('\\'), StringComparison.OrdinalIgnoreCase)) + { + return true; + } + return false; + } + + private IDictionary RegisteredPackageSources + { + get + { + if (_registeredPackageSources == null || !_registeredPackageSources.Any()) + { + _registeredPackageSources = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + try + { + Debug(Resources.Messages.DebugInfoCallMethod3, Constants.ProviderName, "RegisteredPackageSources", ConfigurationFileLocation); + + var config = Config; + if (config != null) + { + // get the configuration node + var configuration = config.ElementsNoNamespace("configuration").FirstOrDefault(); + if (configuration != null) + { + // get the packageSources node + var packageSources = configuration.ElementsNoNamespace("packageSources").FirstOrDefault(); + + if (packageSources != null) + { + _registeredPackageSources = packageSources.Elements("add") + .Where(each => each.Attribute("key") != null && each.Attribute("value") != null) + .ToDictionaryNicely(each => each.Attribute("key").Value, each => + new PackageSource + { + Request = this, + Name = each.Attribute("key").Value, + Location = each.Attribute("value").Value, + Trusted = each.Attribute("trusted") != null && each.Attribute("trusted").Value.IsTrue(), + IsRegistered = true, + IsValidated = each.Attribute("validated") != null && each.Attribute("validated").Value.IsTrue(), + }, StringComparer.OrdinalIgnoreCase); + + } + } + } + } + catch (Exception) + { + _registeredPackageSources = new ConcurrentDictionary(); + } + } + + return _registeredPackageSources; + } + } + + /// + /// Return all registered package sources + /// + internal IEnumerable SelectedSources + { + get + { + if (IsCanceled) + { + yield break; + } + + //get sources from user's input + var sourcesFromUser = (PackageSources ?? Enumerable.Empty()).ToArray(); + //get sources from config file that registered earlier + var pkgSources = RegisteredPackageSources; + + Debug(Resources.Messages.RegisteredSources, pkgSources.Count, Constants.ProviderName); + + //If a user does not provide -source, we use the registered ones + if (sourcesFromUser.Length == 0) + { + // return them all. + foreach (var src in pkgSources.Values) + { + Debug(src.Name); + yield return src; + } + yield break; + } + var userSpecifiesArrayOfSources = sourcesFromUser.Length > 1; + + // otherwise, return package sources that match the items given. + foreach (var src in sourcesFromUser) + { + // Check whether we've already processed this item before + if (_checkedUnregisteredPackageSources.ContainsKey(src)) + { + _checkedUnregisteredPackageSources[src].Request = this; + yield return _checkedUnregisteredPackageSources[src]; + continue; + } + + // check to see if we have a source with either that name + // or that URI first. + if (pkgSources.ContainsKey(src)) + { + Debug(Resources.Messages.FoundRegisteredSource, src, Constants.ProviderName); + _checkedUnregisteredPackageSources.Add(src, pkgSources[src]); + yield return pkgSources[src]; + continue; + } + + var srcLoc = src; + var found = false; + foreach (var byLoc in pkgSources.Values) + { + // srcLoc does not match byLoc.Location, try to check for srcLoc with "/" appended at the end + if (!string.Equals(byLoc.Location, srcLoc, StringComparison.OrdinalIgnoreCase) + && !string.Equals(byLoc.Location, string.Concat(srcLoc, "/"), StringComparison.OrdinalIgnoreCase) + && !string.Equals(string.Concat(byLoc.Location, "/"), srcLoc, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + _checkedUnregisteredPackageSources.Add(srcLoc, byLoc); + yield return byLoc; + found = true; + } + if (found) + { + continue; + } + + Debug(Resources.Messages.NotFoundRegisteredSource, src, Constants.ProviderName); + + // doesn't look like we have this as a source. + if (Uri.IsWellFormedUriString(src, UriKind.Absolute)) + { + // we have been passed in an URI + var srcUri = new Uri(src); + if (SupportedSchemes.Contains(srcUri.Scheme.ToLower())) + { + // it's one of our supported uri types. + var isValidated = false; + + //if (!SkipValidate.Value) + //{ + // isValidated = PathUtility.ValidateSourceUri(SupportedSchemes, srcUri, this); + //} + + if (SkipValidate.Value || isValidated) + { + Debug(Resources.Messages.SuccessfullyValidated, src); + + yield return new PackageSource + { + Request = this, + Location = srcUri.ToString(), + Name = srcUri.ToString(), + Trusted = false, + IsRegistered = false, + IsValidated = isValidated, + }; + continue; + } + if (userSpecifiesArrayOfSources) + { + Verbose(Constants.Messages.UnableToResolveSource, Constants.ProviderName, src); + } + else + { + Warning(Constants.Messages.UnableToResolveSource, Constants.ProviderName, src); + } + + continue; + } + + // Not a valid location? + if (userSpecifiesArrayOfSources) + { + Verbose(Constants.Messages.UnableToResolveSource, Constants.ProviderName, src); + } + else + { + Warning(Constants.Messages.UnableToResolveSource, Constants.ProviderName, src); + } + + continue; + } + + // is it a file path? + if (System.IO.Directory.Exists(src)) + { + Debug(Resources.Messages.SourceIsADirectory, src); + + PackageSource newSource = new PackageSource + { + Request = this, + Location = src, + Name = src, + Trusted = true, + IsRegistered = false, + IsValidated = true, + }; + yield return newSource; + } + else + { + // Not a valid location? + if (userSpecifiesArrayOfSources) + { + Verbose(Constants.Messages.UnableToResolveSource, Constants.ProviderName, src); + } + else + { + Warning(Constants.Messages.UnableToResolveSource, Constants.ProviderName, src); + } + } + } + } + } + + private XDocument Config + { + get + { + if (_config == null) + { + Debug(Resources.Messages.LoadingConfigurationFile, ConfigurationFileLocation); + + XDocument doc = null; + + try + { + using (FileStream fs = new FileStream(ConfigurationFileLocation, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + // load the xdocument from the file stream + doc = XmlUtility.LoadSafe(fs, ignoreWhiteSpace: true); + } + + Debug(Resources.Messages.LoadedConfigurationFile, ConfigurationFileLocation); + + if (doc.Root != null && doc.Root.Name != null && String.Equals(doc.Root.Name.LocalName, "configuration", StringComparison.OrdinalIgnoreCase)) + { + _config = doc; + return _config; + } + Warning(Resources.Messages.MissingConfigurationElement, ConfigurationFileLocation); + } + catch (Exception e) + { + // a bad xml doc or a folder gets deleted somehow + Warning(e.Message); + } + } + + return _config; + } + set + { + + if (value == null) + { + Debug(Resources.Messages.SettingConfigurationToNull); + return; + } + + _config = value; + + Verbose(Resources.Messages.SavingConfigurationWithFile, ConfigurationFileLocation); + var stringBuilder = new System.Text.StringBuilder(); + + var settings = new XmlWriterSettings + { + OmitXmlDeclaration = false, + Indent = true, + NewLineOnAttributes = true, + NamespaceHandling = NamespaceHandling.OmitDuplicates + }; + + using (var xmlWriter = XmlWriter.Create(stringBuilder, settings)) + { + _config.Save(xmlWriter); + System.IO.File.WriteAllText(ConfigurationFileLocation, _config.ToString()); + } + } + } + + private string ConfigurationFileLocation + { + get + { + if (String.IsNullOrWhiteSpace(_configurationFileLocation)) + { + // get the value from the request + var path = GetOptionValue("ConfigFile"); + + if (!String.IsNullOrWhiteSpace(path)) + { + _configurationFileLocation = path; + + if (!System.IO.File.Exists(_configurationFileLocation)) + { + WriteError(Internal.ErrorCategory.InvalidArgument, _configurationFileLocation, Resources.Messages.FileNotFound); + } + + } + else + { + var appdataFolder = Environment.GetEnvironmentVariable("appdata"); + _configurationFileLocation = Path.Combine(appdataFolder, Constants.ProviderName, Constants.SettingsFileName); + + //create directory if does not exist + string dir = Path.GetDirectoryName(_configurationFileLocation); + if (dir != null && !System.IO.Directory.Exists(dir)) + { + Debug(Resources.Messages.CreateDirectory, dir); + System.IO.Directory.CreateDirectory(dir); + } + //create place holder config file + if (!System.IO.File.Exists(_configurationFileLocation)) + { + Debug(Resources.Messages.CreateFile, _configurationFileLocation); + System.IO.File.WriteAllText(_configurationFileLocation, DefaultConfig); + } + } + } + + return _configurationFileLocation; + } + } + + + internal bool WriteError(Internal.ErrorCategory category, string targetObjectValue, string messageText, params object[] args) + { + return Error(messageText, category.ToString(), targetObjectValue, base.FormatMessageString(messageText, args)); + } + + /// + /// Check if the package source location is valid + /// + /// + /// + internal bool ValidateSourceLocation(string location) + { + Debug(Resources.Messages.DebugInfoCallMethod3, _PackageSourceListRequest, "ValidateSourceLocation", location); + + //TODO this is needed for register-packagesource + return true; + } + + internal static IRequest ExtendRequest(Dictionary options, string[] sources, bool isTrusted, PackageSourceListRequest request) { + var srcs = sources ?? new List().ToArray(); + var opts = options ?? new Dictionary(); + + return (new object[] + { + new { + GetOptionKeys = new Func>(() => opts.Keys.Any() ? request.OptionKeys.Concat(opts.Keys) : request.OptionKeys), + GetOptionValues = new Func>((key) => + { + if (opts.ContainsKey(key)) + { + return opts[key]; + } + return request.GetOptionValues(key); + }), + + GetSources = new Func>(() => srcs), + //ShouldContinueWithUntrustedPackageSource = new Func((pkgName, pkgSource) => isTrusted), + }, + request + }).As(); + } + + /// + /// HttpClient with Accept-CharSet and Accept-Encoding Header + /// We want to reuse HttpClient + /// + internal HttpClient Client + { + get + { + if (_httpClient == null) + { + _httpClient = PathUtility.GetHttpClientHelper(CredentialUsername, CredentialPassword, WebProxy); + + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept-Charset", "UTF-8"); + // Request for gzip and deflate encoding to make the response lighter. + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept-Encoding", "gzip,deflate"); + + // needed for downloading package like node.js.msi + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "text / html, application / xhtml + xml, image / jxr, */*"); + + foreach (var header in Headers.Value) + { + // header is in the format "A=B" because OneGet doesn't support Dictionary parameters + if (!string.IsNullOrEmpty(header)) + { + var headerSplit = header.Split(new string[] { "=" }, 2, StringSplitOptions.RemoveEmptyEntries); + + // ignore wrong entries + if (headerSplit.Count() == 2) + { + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation(headerSplit[0], headerSplit[1]); + } + else + { + Warning(Resources.Messages.HeaderIgnored, header); + } + } + } + } + + return _httpClient; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PackageQuery.cs b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PackageQuery.cs new file mode 100644 index 000000000..4a2a14c9f --- /dev/null +++ b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PackageQuery.cs @@ -0,0 +1,154 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +namespace Microsoft.PackageManagement.PackageSourceListProvider +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Microsoft.PackageManagement.Internal.Packaging; + using Microsoft.PackageManagement.Provider.Utility; + using System.Globalization; + internal class PackageQuery + { + private Dictionary> _PackageSourceList; + + private PackageQuery() { } + + internal virtual bool IsValid + { + get + { + return _PackageSourceList != null; + } + } + internal PackageQuery(PackageSource packageSource, PackageSourceListRequest request) + { + if (packageSource == null) + { + throw new ArgumentNullException("packageSource"); + } + + Uri uri; + + if (Uri.TryCreate(packageSource.Location, UriKind.Absolute, out uri)) + { + if (uri.IsFile) + { + try + { + //process the file + _PackageSourceList = JsonParser.ReadPackageSpec(packageSource, null); + } + catch (Exception ex) + { + request.Warning(string.Format(CultureInfo.CurrentCulture, Resources.Messages.InvalidPackageListFormat, uri.AbsolutePath)); + ex.Dump(request); + } + } + + // Uri? + //TODO: ask a user whether go ahead for downloading an sample PSL.json + } + else + { + //TODO: Check with GP, DSC Settings, + + request.Verbose(Resources.Messages.UnsupportedPackageSource, packageSource.Location); + return; + } + } + + internal PackageQuery(PackageSource packageSource, string file, PackageSourceListRequest request) + { + try + { + //process the file + _PackageSourceList = JsonParser.ReadPackageSpec(packageSource, file); + } + catch (Exception ex) + { + request.Warning(string.Format(CultureInfo.CurrentCulture, Resources.Messages.InvalidPackageListFormat, file)); + ex.Dump(request); + } + } + + internal IEnumerable Query() + { + return _PackageSourceList.SelectMany(each => each.Value).Where(item => !item.IsCommonDefinition); ; + + } + + internal IEnumerable Query(string name) + { + return + _PackageSourceList.SelectMany(each => each.Value) + .Where(item => (item.Name.EqualsIgnoreCase(name) || (!string.IsNullOrWhiteSpace(item.DisplayName) && item.DisplayName.EqualsIgnoreCase(name))) && !item.IsCommonDefinition); + + } + + internal IEnumerable Query(string name, string version) + { + return string.IsNullOrWhiteSpace(version) + ? _PackageSourceList.SelectMany(each => each.Value).Where(item => (item.Name.EqualsIgnoreCase(name) || + (!string.IsNullOrWhiteSpace(item.DisplayName) && item.DisplayName.EqualsIgnoreCase(name))) && !item.IsCommonDefinition) + : _PackageSourceList.SelectMany(each => each.Value) + .Where(item => + (item.Name.EqualsIgnoreCase(name) || (!string.IsNullOrWhiteSpace(item.DisplayName) && item.DisplayName.EqualsIgnoreCase(name))) + && !item.IsCommonDefinition + && (string.IsNullOrWhiteSpace(item.Version) || new SemanticVersion(item.Version) == new SemanticVersion(version))); + } + + internal IEnumerable Query(string name, string minimumVersion, string maximumVersion) + { + if (string.IsNullOrEmpty(minimumVersion) && string.IsNullOrEmpty(maximumVersion)) + { + return Query(name); + } + + if (!IsValid || string.IsNullOrEmpty(name)) + { + return Enumerable.Empty(); + } + + var packages = _PackageSourceList.SelectMany(each => each.Value).Where(item => (item.Name.EqualsIgnoreCase(name) || + (!string.IsNullOrWhiteSpace(item.DisplayName) && item.DisplayName.EqualsIgnoreCase(name))) && !item.IsCommonDefinition); + + return packages.Where(pkg => { + + if (!string.IsNullOrWhiteSpace(minimumVersion)) + { + if (SoftwareIdentityVersionComparer.CompareVersions(pkg.VersionScheme, pkg.Version, minimumVersion) < 0) + { + // a minimum version was specified, but the package version is less than the specified minimumversion. + return false; + } + } + + if (!string.IsNullOrWhiteSpace(maximumVersion)) + { + if (SoftwareIdentityVersionComparer.CompareVersions(pkg.VersionScheme, pkg.Version, maximumVersion) > 0) + { + // a maximum version was specified, but the package version is more than the specified maximumversion. + return false; + } + } + + // the version is in the range asked for. + return true; + }); + } + } +} diff --git a/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PackageSource.cs b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PackageSource.cs new file mode 100644 index 000000000..7e84a7278 --- /dev/null +++ b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PackageSource.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +namespace Microsoft.PackageManagement.PackageSourceListProvider +{ + using Microsoft.PackageManagement.Internal.Implementation; + using Microsoft.PackageManagement.Provider.Utility; + internal class PackageSource + { + //Parameters will be filled during the instantiation. + internal string Name { get; set; } + + internal string Location { get; set; } + + internal bool Trusted { get; set; } + + internal bool IsRegistered { get; set; } + + internal bool IsValidated { get; set; } + + internal Request Request { get; set; } + + internal string Serialized + { + get + { + return Location.ToBase64(); + } + } + } +} diff --git a/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PowerShellArtifactInstaller.cs b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PowerShellArtifactInstaller.cs new file mode 100644 index 000000000..547d14c00 --- /dev/null +++ b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/PowerShellArtifactInstaller.cs @@ -0,0 +1,200 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +namespace Microsoft.PackageManagement.PackageSourceListProvider +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Globalization; + using Microsoft.PackageManagement.Internal.Api; + using Microsoft.PackageManagement.Packaging; + using Microsoft.PackageManagement.Provider.Utility; + using Microsoft.PackageManagement.Internal.Utility.Plugin; + internal static class PowerShellArtifactInstaller + { + internal static void InstallPowershellArtifacts(PackageJson package, string fastPath, PackageSourceListRequest request) + { + var provider1 = PackageSourceListRequest.FindProvider(request, package.Type, request, true); + if (provider1 == null) return; + + // As the PowerShellGet may access the -source via $request.Options or PackageSources, + // so we need to fill in both options and sources parameters + var installRequest = PackageSourceListRequest.ExtendRequest(new Dictionary + { + {"Source", new[] {package.Source ?? ""}} + }, new[] { package.Source ?? "" }, package.IsTrustedSource, request); + + var psid = PackageSourceListRequest.CreateCanonicalId(package, Constants.ProviderNames.PowerShellGet); + var pkgs = request.PackageManagementService.FindPackageByCanonicalId(psid, installRequest) + .Where(each => new SemanticVersion(each.Version) == new SemanticVersion(package.Version)).ToArray(); + + switch (pkgs.Length) + { + case 0: + request.Warning(Resources.Messages.CannotFindPackage, Constants.ProviderName, psid); + break; + case 1: + InstallPackageViaPowerShellGet(package, request, pkgs); + break; + + default: + request.Warning(Resources.Messages.FoundMorePackages, Constants.ProviderName, pkgs.Length, psid); + break; + } + return; + } + + private static void InstallPackageViaPowerShellGet(PackageJson packageJson, PackageSourceListRequest request, SoftwareIdentity[] packages) + { + + var provider = PackageSourceListRequest.FindProvider(request, packageJson.Type, request, true); + if (provider == null) return; + + IHostApi installRequest = request; + + if (provider.Name.EqualsIgnoreCase(Constants.ProviderNames.PowerShellGet) && !request.ProviderServices.IsElevated) { + // if we're not elevated, we want powershellget to install to the user scope + installRequest = PackageSourceListRequest.ExtendRequest( + new Dictionary { + {"Scope", new[] {"CurrentUser"}} + }, null, packageJson.IsTrustedSource, request); + + } else { + installRequest = PackageSourceListRequest.ExtendRequest( + new Dictionary { + {"Destination", new[] {packageJson.Destination ?? ""}} + }, null, packageJson.IsTrustedSource, request); + } + + request.Debug("Calling '{0}' provider to install the package '{1}.{2}'", provider.Name, packageJson.Name, packageJson.Version); + + var installing = provider.InstallPackage(packages[0], installRequest); + + if (installing == null || !installing.Any()) + { + request.Verbose(Resources.Messages.NumberOfPackagesRecevied, 0, provider.Name, "InstallPackage"); + request.Warning(Resources.Messages.FailToInstallPackage, Constants.ProviderName, packages[0].Name); + return; + } + + int packagesRecevied = 0; + foreach (var i in installing) + { + request.YieldSoftwareIdentity(i.FastPackageReference, i.Name, i.Version, i.VersionScheme, i.Summary, i.Source, i.SearchKey, i.FullPath, i.PackageFilename); + + if (request.IsCanceled) + { + installing.Cancel(); + } + else + { + request.Verbose(Resources.Messages.SuccessfullyInstalled, "{0}.{1}".format(packageJson.Name, packageJson.Version)); + //load provider + if (packageJson.IsPackageProvider) + { + //Per provider development guidance: provider name and module name should be the same otherwise we can not import it. + request.PackageManagementService.ImportPackageProvider(request, packageJson.Name, null, null, null, isRooted: false, force: false); + } + } + packagesRecevied++; + } + + request.Verbose(Resources.Messages.NumberOfPackagesRecevied, packagesRecevied, provider.Name, "install-package"); + } + + internal static void GeInstalledPowershellArtifacts(PackageJson package, string requiredVersion, string minimumVersion, string maximumVersion, Dictionary fastPackReftable, PackageSourceListRequest request) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, Constants.ProviderName, string.Format(CultureInfo.InvariantCulture, "GeInstalledPowershellArtifacts' - name='{0}', requiredVersion='{1}',minimumVersion='{2}', maximumVersion='{3}'", package.Name, requiredVersion, minimumVersion, maximumVersion)); + + var provider = PackageSourceListRequest.FindProvider(request, package.Type, request); + if (provider == null) return; + + //calling the PowerShellGet provider + request.Debug("Calling '{0}' provider to get installed packages '{1}.{2}'", provider.Name, package.Name, package.Version); + + var packagesInstalled = provider.GetInstalledPackages(package.Name, requiredVersion, minimumVersion, maximumVersion, request).ToArray(); + + if (packagesInstalled == null || !packagesInstalled.Any()) + { + request.Verbose(Resources.Messages.NumberOfPackagesRecevied, 0, provider.Name, "GetInstalledPackages"); + return; + } + + foreach (var i in packagesInstalled) + { + request.Debug("Found an installed package '{0}.{1} from {2}' ", i.Name, i.Version, i.Source); + var info = PackageSourceListRequest.MakeFastPathComplex(i.Source, i.Name, "", i.Version, ""); + + fastPackReftable.AddOrSet(info, i); + + // check if the installed version matches with the one specified in the PSL.json. + // If so, we choose PSL.json. + var version = i.Version.CompareVersion(package.Version) ? package.Version : i.Version; + request.YieldSoftwareIdentity(info, i.Name, version, i.VersionScheme, i.Summary, i.Source, i.SearchKey, i.FullPath, i.PackageFilename); + } + } + + /// + /// Uninstalls a package + /// + /// package defined in the PackageSourceList + /// + /// An object passed in from the PackageManagement that contains functions that can be used to interact with its Provider + /// + internal static void UninstallPowershellArtifacts(PackageJson package, string fastPackageReference, PackageSourceListRequest request, Dictionary fastPackReftable) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + request.Debug(Resources.Messages.DebugInfoCallMethod, Constants.ProviderName, string.Format(CultureInfo.InvariantCulture, "UninstallNuGetPackage' - fastReference='{0}'", fastPackageReference)); + + var provider = PackageSourceListRequest.FindProvider(request, package.Type, request, true); + if (provider != null) + { + request.Debug("{0}: Using the provider '{1} to uninstall the package '{2}'", Constants.ProviderName, provider.Name, package.Name); + + if (!fastPackReftable.ContainsKey(fastPackageReference)) + { + request.WriteError(Internal.ErrorCategory.InvalidData, fastPackageReference, Resources.Messages.FailedToGetPackageObject, Constants.ProviderName, fastPackageReference); + return; + } + + var p = fastPackReftable[fastPackageReference]; + + //calling NuGet for uninstall + var installing = provider.UninstallPackage(p, request); + + foreach (var i in installing) + { + request.YieldSoftwareIdentity(i.FastPackageReference, i.Name, i.Version, i.VersionScheme, i.Summary, i.Source, i.SearchKey, i.FullPath, i.PackageFilename); + + if (request.IsCanceled) + { + installing.Cancel(); + } + } + } + } + + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/ProgressTracker.cs b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/ProgressTracker.cs new file mode 100644 index 000000000..711ae473e --- /dev/null +++ b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/ProgressTracker.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +namespace Microsoft.PackageManagement.PackageSourceListProvider +{ + using Microsoft.PackageManagement.Internal.Implementation; + + internal class ProgressTracker + { + internal int ProgressID; + internal int StartPercent; + internal int EndPercent; + + internal ProgressTracker(int progressID) : this(progressID, 0, 100) { } + + internal ProgressTracker(int progressID, int startPercent, int endPercent) + { + ProgressID = progressID; + StartPercent = startPercent; + EndPercent = endPercent; + } + + internal int ConvertPercentToProgress(double percent) + { + // for example, if startprogress is 50, end progress is 100, and if we want to complete 50% of that, + // then the progress returned would be 75 + return StartPercent + (int)((EndPercent - StartPercent) * percent); + } + + internal static ProgressTracker StartProgress(ProgressTracker parentTracker, string message, Request request) + { + if (request == null) + { + return null; + } + + // if parent tracker is null, use 0 for parent id, else use the progressid of parent tracker + return new ProgressTracker(request.StartProgress(parentTracker == null ? 0 : parentTracker.ProgressID, message)); + } + } +} diff --git a/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/WebDownloader.cs b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/WebDownloader.cs new file mode 100644 index 000000000..4ab3332b6 --- /dev/null +++ b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/WebDownloader.cs @@ -0,0 +1,240 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +namespace Microsoft.PackageManagement.PackageSourceListProvider +{ + using System; + using System.IO; + using System.Net; + using System.Threading; + using Directory = System.IO.Directory; + using File = System.IO.File; + using System.Threading.Tasks; + using Microsoft.PackageManagement.Provider.Utility; + + public class WebDownloader { + + /// + /// Download data from remote via uri query. + /// + /// A file to store the downloaded data. + /// Uri query + /// An object passed in from the PackageManagement platform that contains APIs that can be used to interact with it + /// Credential to pass along to get httpclient + /// Utility class to help track progress + /// + internal static async Task DownloadDataToFileAsync(string fileName, string query, PackageSourceListRequest request, NetworkCredential networkCredential, ProgressTracker progressTracker) + { + request.Verbose(Resources.Messages.DownloadingPackage, query); + + // try downloading for 3 times + int remainingTry = 3; + long totalDownloaded = 0; + + CancellationTokenSource cts; + Stream input = null; + FileStream output = null; + + while (remainingTry > 0) + { + // if user cancel the request, no need to do anything + if (request.IsCanceled) + { + break; + } + + input = null; + output = null; + cts = new CancellationTokenSource(); + totalDownloaded = 0; + + try + { + // decrease try by 1 + remainingTry -= 1; + + var httpClient = request.Client; + + httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "text/html; charset=iso-8859-1"); + + input = await httpClient.GetStreamAsync(query); + + + // buffer size of 64 KB, this seems to be preferable buffer size, not too small and not too big + byte[] bytes = new byte[1024 * 64]; + output = File.Open(fileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read); + + int current = 0; + + // here we read content that we got from the http response stream into the bytes array + current = await input.ReadAsync(bytes, 0, bytes.Length, cts.Token); + + // report initial progress + request.Progress(progressTracker.ProgressID, progressTracker.StartPercent, Resources.Messages.BytesRead, current); + + while (current > 0) + { + totalDownloaded += current; + + // here we write out the bytes array into the file + await output.WriteAsync(bytes, 0, current, cts.Token); + + // report the progress + request.Progress(progressTracker.ProgressID, progressTracker.StartPercent, Resources.Messages.BytesRead, totalDownloaded); + + // continue reading from the stream + current = await input.ReadAsync(bytes, 0, bytes.Length, cts.Token); + } + + if (totalDownloaded > 0) + { + // report that we finished the download + request.Progress(progressTracker.ProgressID, progressTracker.EndPercent, Resources.Messages.BytesRead, totalDownloaded); + request.CompleteProgress(progressTracker.ProgressID, true); + + return totalDownloaded; + } + + // if request is canceled, don't retry + if (request.IsCanceled) + { + return 0; + } + } + catch (Exception ex) + { + request.CompleteProgress(progressTracker.ProgressID, true); + + request.Verbose(ex.Message); + request.Debug(ex.StackTrace); + } + finally + { + // dispose input and output stream + if (input != null) + { + input.Dispose(); + } + + // dispose it + if (output != null) + { + output.Dispose(); + } + // delete the file if created and nothing has downloaded + if (totalDownloaded == 0 && File.Exists(fileName)) + { + fileName.TryHardToDelete(); + } + + if (cts != null) + { + cts.Dispose(); + } + } + + // we have to retry again + request.Verbose(Resources.Messages.RetryingDownload, query, remainingTry); + } + + return totalDownloaded; + } + + internal static string DownloadFile(string queryUrl, string destination, PackageSourceListRequest request, ProgressTracker progressTracker) + { + try + { + request.Debug(Resources.Messages.DebugInfoCallMethod, Constants.ProviderName, string.Format(System.Globalization.CultureInfo.InvariantCulture, "DownloadFile - url='{0}', destination='{1}'", queryUrl, destination)); + + if (string.IsNullOrWhiteSpace(destination)) + { + throw new ArgumentNullException("destination"); + } + + // make sure that the parent folder is created first. + var folder = Path.GetDirectoryName(destination); + if (!Directory.Exists(folder)) + { + Directory.CreateDirectory(folder); + } + + if (File.Exists(destination)) + { + destination.TryHardToDelete(); + } + + + if (progressTracker == null) + { + progressTracker = new ProgressTracker(request.StartProgress(0, Resources.Messages.DownloadingPackage, queryUrl)); + } + + + Uri uri; + + if (!Uri.TryCreate(queryUrl, UriKind.Absolute, out uri)) + { + request.Error(Internal.ErrorCategory.InvalidOperation, Resources.Messages.UnsuportedUriFormat, Constants.ProviderName, queryUrl); + return null; + } + + if (uri.IsFile) + { + // downloading from a file share + using (var input = File.OpenRead(queryUrl)) + { + using (var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + request.Progress(progressTracker.ProgressID, progressTracker.StartPercent, Resources.Messages.Downloading); + + input.CopyTo(output); + + } + } + + request.CompleteProgress(progressTracker.ProgressID, true); + } + else + { + //Downloading from url + var result = DownloadDataToFileAsync(destination, queryUrl, request, PathUtility.GetNetworkCredential(request.CredentialUsername, request.CredentialPassword), progressTracker).Result; + } + + if (File.Exists(destination)) + { + request.Verbose(Resources.Messages.CompletedDownload, queryUrl); + return destination; + } + else + { + request.Error(Internal.ErrorCategory.InvalidOperation, Resources.Messages.FailedToDownload, Constants.ProviderName, queryUrl, destination); + return null; + } + } + catch (Exception ex) + { + ex.Dump(request); + request.Warning(ex.Message); + return null; + } + } + + internal static bool PerformSecurityScan(string fileFullPath) + { + //TODO need to do security scan before running install + return true; + } + } +} diff --git a/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/ZipPackageInstaller.cs b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/ZipPackageInstaller.cs new file mode 100644 index 000000000..f81594518 --- /dev/null +++ b/src/Microsoft.PackageManagement.PackageSourceListProvider/PackageList/ZipPackageInstaller.cs @@ -0,0 +1,215 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + + +namespace Microsoft.PackageManagement.PackageSourceListProvider +{ + using System; + using System.Globalization; + using System.IO; + using System.Linq; + using System.IO.Compression; + using System.Threading; + using Microsoft.PackageManagement.Provider.Utility; + using Microsoft.PackageManagement.Internal; + internal static class ZipPackageInstaller { + internal static void GetInstalledZipPackage(PackageJson package, PackageSourceListRequest request) + { + + string path = Path.Combine(package.Destination, package.Name, package.Version); + if (Directory.Exists(path)) + { + if (Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories).Any()) + { + var fp = PackageSourceListRequest.MakeFastPathComplex(package.Source, package.Name, (package.DisplayName ?? ""), package.Version, path); + + //the directory exists and contain files, we think the package has been installed. + request.YieldSoftwareIdentity(fp, package.Name, package.Version, package.VersionScheme, package.Summary, path, package.Name, path, path); + } + } + } + internal static bool InstallZipPackage(PackageJson package, string fastPath, PackageSourceListRequest request) + { + // download the exe package + var file = Path.ChangeExtension(Path.GetTempFileName(), "exe"); + + WebDownloader.DownloadFile(package.Source, file, request, null); + if (!File.Exists(file)) + { + return false; + } + + // validate the file + if (!WebDownloader.PerformSecurityScan(file)) + { + //TODO Security Scan work + request.WriteError(ErrorCategory.InvalidOperation, Constants.ProviderName, "The package download from '{0}' failed in the security scan.", package.Source); + return false; + } + + if (!package.IsTrustedSource) + { + if (!request.ShouldContinueWithUntrustedPackageSource(package.Name, package.Source)) + { + request.Warning(Constants.Messages.UserDeclinedUntrustedPackageInstall, package.Name); + return false; + } + } + + + Timer timer = null; + object timerLock = new object(); + bool cleanUp = false; + + ProgressTracker tracker = new ProgressTracker(request.StartProgress(0, "Installing Zip Package............")); + double percent = tracker.StartPercent; + Action cleanUpAction = () => { + lock (timerLock) + { + // check whether clean up is already done before or not + if (!cleanUp) + { + try + { + if (timer != null) + { + // stop timer + timer.Change(Timeout.Infinite, Timeout.Infinite); + timer.Dispose(); + timer = null; + } + } + catch + { + } + + cleanUp = true; + } + } + }; + + // extracted folder + string extractedFolder = string.Concat(file.GenerateTemporaryFilename()); + + try + { + + timer = new Timer(_ => + { + percent += 0.025; + var progressPercent = tracker.ConvertPercentToProgress(percent); + if (progressPercent < 90) + { + request.Progress(tracker.ProgressID, (int)progressPercent, string.Format(CultureInfo.CurrentCulture, "Copying files ...")); + } + if (request.IsCanceled) + { + cleanUpAction(); + } + }, null, 0, 1000); + + //unzip the file + ZipFile.ExtractToDirectory(file, extractedFolder); + + if (Directory.Exists(extractedFolder)) + { + + var versionFolder = Path.Combine(package.Destination, package.Name, package.Version); + + // create the directory version folder if not exist + if (!Directory.Exists(versionFolder)) + { + Directory.CreateDirectory(versionFolder); + } + + try + { + FileUtility.CopyDirectory(extractedFolder, versionFolder, true); + request.YieldFromSwidtag(package, fastPath); + } + catch (Exception e) + { + request.CompleteProgress(tracker.ProgressID, false); + request.Debug(e.StackTrace); + + if (!(e is UnauthorizedAccessException || e is IOException)) + { + // something wrong, delete the version folder + versionFolder.TryHardToDelete(); + return false; + } + } + return true; + } + else + { + request.Warning("Failed to download a Zip package {0} from {1}", package.Name, package.Source); + } + } + finally + { + cleanUpAction(); + file.TryHardToDelete(); + extractedFolder.TryHardToDelete(); + request.CompleteProgress(tracker.ProgressID, true); + + } + + return false; + } + + internal static bool UnInstallZipPackage(PackageSourceListRequest request, string fastPath) + { + string sourceLocation; + string id; + string displayName; + string version; + string path; + + var package = request.GetFastReferenceComplex(fastPath); + + if (!request.TryParseFastPathComplex(fastPath: fastPath, regex: PackageSourceListRequest.RegexFastPathComplex, location: out sourceLocation, id: out id, displayname: out displayName, version: out version, fastpath: out path)) + { + request.Error(ErrorCategory.InvalidOperation, "package", Constants.Messages.UnableToUninstallPackage); + return false; + } + + if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path)) + { + path.TryHardToDelete(); + request.YieldFromSwidtag(package, path); + return true; + } + else + { + request.Error(ErrorCategory.InvalidData, "folder {0} does not exist", path); + return false; + } + } + + internal static void DownloadZipPackage(string fastPath, string location, PackageSourceListRequest request) + { + if (request == null) + { + throw new ArgumentNullException("request"); + } + + //TODO + request.Debug(Resources.Messages.DebugInfoCallMethod, Constants.ProviderName, + string.Format(CultureInfo.InvariantCulture, "DownloadZipPackage' - fastReference='{0}', location='{1}'", fastPath, location)); + + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement.PackageSourceListProvider/Properties/AssemblyInfo.cs b/src/Microsoft.PackageManagement.PackageSourceListProvider/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..7e532ae3a --- /dev/null +++ b/src/Microsoft.PackageManagement.PackageSourceListProvider/Properties/AssemblyInfo.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Microsoft.PackageManagement.PackageSourceListProvider")] +[assembly: AssemblyDescription("PSL")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("PackageManagement")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("6141d4af-6ed6-4cfb-8b23-125e8909e873")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.0.0.210")] +[assembly: AssemblyFileVersion("1.0.0.210")] diff --git a/src/Microsoft.PackageManagement.PackageSourceListProvider/Sdk/Constants.cs b/src/Microsoft.PackageManagement.PackageSourceListProvider/Sdk/Constants.cs new file mode 100644 index 000000000..d59cb18bb --- /dev/null +++ b/src/Microsoft.PackageManagement.PackageSourceListProvider/Sdk/Constants.cs @@ -0,0 +1,155 @@ + +namespace Microsoft.PackageManagement.PackageSourceListProvider +{ + public static class Constants + { + /// + /// Name of this provider + /// + public const string ProviderName = "PSL"; + + /// + /// Version of this provider + /// + public static readonly string ProviderVersion = "1.0.0.210"; + + /// + /// Config file storing the info as a result of registering a package source + /// + public static readonly string SettingsFileName = "PSL.config"; + + internal static class MediaType + { + public const string MsiPackage = "msi"; + public const string MsuPackage = "msu"; + + public const string ExePackage = "exe"; + public const string ZipPackage = "zip"; + public const string NuGetPackage = "nupkg"; + public const string AppxPackage = "appx"; + public const string PsArtifacts = "psartifacts"; + + } + + internal static class ProviderNames + { + public const string PowerShellGet = "PowerShellGet"; + public const string NuGet = "NuGet"; + public const string Msi = "MSI"; + public const string Msu = "MSU"; + public const string Programs = "Programs"; + public const string PSL = ProviderName; + + } + + #region copy common-constants-implementation /internal/public + + internal static string[] Empty = new string[0]; + internal const string MinVersion = "0.0.0.1"; + internal const string MSGPrefix = "MSG:"; + internal const string Download = "Download"; + internal const string Install = "Install"; + public readonly static string[] FeaturePresent = new string[0]; + internal const string CurrentUser = "CurrentUser"; + internal const string AllUsers = "AllUsers"; + + + internal static class Features + { + public const string AutomationOnly = "automation-only"; + public const string MagicSignatures = "magic-signatures"; + public const string SupportedExtensions = "file-extensions"; + public const string SupportedSchemes = "uri-schemes"; + public const string PackageManagementMinimumVersion = "packagemanagement-minimum-version"; + public const string SupportsPowerShellModules = "supports-powershell-modules"; + public const string SupportsRegexSearch = "supports-regex-search"; + public const string SupportsSubstringSearch = "supports-substring-search"; + public const string SupportsWildcardSearch = "supports-wildcard-search"; + } + + internal static class Messages + { + public const string UnableToFindDependencyPackage = "MSG:UnableToFindDependencyPackage"; + public const string MissingRequiredParameter = "MSG:MissingRequiredParameter"; + public const string PackageFailedInstallOrDownload = "MSG:PackageFailedInstallOrDownload"; + public const string PackageSourceExists = "MSG:PackageSourceExists"; + public const string ProtocolNotSupported = "MSG:ProtocolNotSupported"; + public const string ProviderPluginLoadFailure = "MSG:ProviderPluginLoadFailure"; + public const string ProviderSwidtagUnavailable = "MSG:ProviderSwidtagUnavailable"; + public const string RemoveEnvironmentVariableRequiresElevation = "MSG:RemoveEnvironmentVariableRequiresElevation"; + public const string SchemeNotSupported = "MSG:SchemeNotSupported"; + public const string SourceLocationNotValid = "MSG:SourceLocationNotValid"; + public const string UnableToCopyFileTo = "MSG:UnableToCopyFileTo"; + public const string UnableToCreateShortcutTargetDoesNotExist = "MSG:UnableToCreateShortcutTargetDoesNotExist"; + public const string UnableToDownload = "MSG:UnableToDownload"; + public const string UnableToOverwriteExistingFile = "MSG:UnableToOverwriteExistingFile"; + public const string UnableToRemoveFile = "MSG:UnableToRemoveFile"; + public const string UnableToResolvePackage = "MSG:UnableToResolvePackage"; + public const string UnableToResolveSource = "MSG:UnableToResolveSource"; + public const string UnableToUninstallPackage = "MSG:UnableToUninstallPackage"; + public const string UnknownFolderId = "MSG:UnknownFolderId"; + public const string UnknownProvider = "MSG:UnknownProvider"; + public const string UnsupportedArchive = "MSG:UnsupportedArchive"; + public const string UnsupportedProviderType = "MSG:UnsupportedProviderType"; + public const string UriSchemeNotSupported = "MSG:UriSchemeNotSupported"; + public const string UserDeclinedUntrustedPackageInstall = "MSG:UserDeclinedUntrustedPackageInstall"; + public const string HashNotFound = "MSG:HashNotFound"; + public const string HashNotMatch = "MSG:HashNotMatch"; + public const string HashNotSupported = "MSG:HashNotSupported"; + public const string DependencyLoopDetected = "MSG:DependencyLoopDetected"; + public const string CouldNotGetResponseFromQuery = "MSG:CouldNotGetResponseFromQuery"; + public const string SkippedDownloadedPackage = "MSG:SkippedDownloadedPackage"; + public const string InstallRequiresCurrentUserScopeParameterForNonAdminUser = "MSG:InstallRequiresCurrentUserScopeParameterForNonAdminUser"; + + } + + internal static class OptionType { + public const string String = "String"; + public const string StringArray = "StringArray"; + public const string Int = "Int"; + public const string Switch = "Switch"; + public const string Folder = "Folder"; + public const string File = "File"; + public const string Path = "Path"; + public const string Uri = "Uri"; + public const string SecureString = "SecureString"; + } + + internal static class Status + { + internal const string TimedOut = "TimedOut"; + } + + + + internal static class PackageStatus + { + public const string Available = "Available"; + public const string Dependency = "Dependency"; + public const string Installed = "Installed"; + public const string Uninstalled = "Uninstalled"; + } + + internal static class Parameters + { + public const string IsUpdate = "IsUpdatePackageSource"; + public const string Name = "Name"; + public const string Location = "Location"; + } + + internal static class Signatures + { + public const string Cab = "4D534346"; + public const string OleCompoundDocument = "D0CF11E0A1B11AE1"; + public const string Zip = "504b0304"; + //public static string[] ZipVariants = new[] {Zip, /* should have EXEs? */}; + } + + internal static class SwidTag + { + public const string SoftwareIdentity = "SoftwareIdentity"; + } + + #endregion + } +} diff --git a/src/Microsoft.PackageManagement.PackageSourceListProvider/provider.manifest b/src/Microsoft.PackageManagement.PackageSourceListProvider/provider.manifest new file mode 100644 index 000000000..29633468a --- /dev/null +++ b/src/Microsoft.PackageManagement.PackageSourceListProvider/provider.manifest @@ -0,0 +1,8 @@ + + + + diff --git a/src/Microsoft.PackageManagement.PackageSourceListProvider/resources/Microsoft.PackageManagement.PackageSourceListProvider.Resources.Messages.resx b/src/Microsoft.PackageManagement.PackageSourceListProvider/resources/Microsoft.PackageManagement.PackageSourceListProvider.Resources.Messages.resx new file mode 100644 index 000000000..78d8ce1ca --- /dev/null +++ b/src/Microsoft.PackageManagement.PackageSourceListProvider/resources/Microsoft.PackageManagement.PackageSourceListProvider.Resources.Messages.resx @@ -0,0 +1,395 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Package '{0}' is already installed. + + + Completed downloading '{0}'. + + + Completed iterating for '{0}'. + + + Could not get response from query '{0}'. + + + Create directory '{0}'. + + + Create a file '{0}'. + + + Calling '{0}'::'{1}'. + + + Calling '{0}'::'{1}', '{2}'. + + + Returning the call '{0}'::'{1}'. + + + Dependency loop detected for package '{0}'. + + + Downloading ... + + + '{0}': Downloading package '{1}' failed, please make sure '{1}' is accessable. + + + File '{0}' does not exist in '{1}'. + + + Source '{0}' is one of the registered sources in '{1}' provider. + + + Found '{0}' registered package source in '{1}' provider. + + + Hash not found for the package '{0}'. + + + Hash for package '{0}' does not match hash provided from the server. + + + Hash algorithm '{0}' is not supported. + + + Provided header value '{0}' was invalid and hence ignored. + + + The file extension '{0}' is not valid. The required file extension is '.nupkg'. + + + The package '{0}' does not have the .nuspec file. + + + Query Url {0} is invalid. + + + '{0}' is not a valid version string. + + + Loaded a configuration from file '{0}'. + + + Loading a configuration from file '{0}'. + + + Configuration file '{0}' does not have 'configuration' element. + + + '{0}': No package found. + 0 - provider name + + + '{0}': Failed to install Package '{1}'. + 0 - provider name. 1 - package name + + + Source '{0}' is not one of the registered sources in '{1}' provider. + + + Package '{0}' failed to install. + + + '{0}' packages recevied in the last request. + + + A required provider '{0}' does not exist. Try 'Find-PackageProvider' and 'Install-PackageProvider' to install the particular provider. + 0 - provider name + + + There are '{0}' registered sources in '{1}' provider. + + + Removed the package source: '{0}'. + + + Request is canceled in '{0}'::'{1}'. + + + Saving a config with a file path '{0}'. + + + Searching repository '{0}' for '{1}'. + + + Setting a config object to null. + + + Skipping Downloaded Package '{0}'. + + + Source '{0}' is a directory. + + + Source '{0}' is a file path. + + + Package '{0}' is downloaded. + + + Package '{0}' is installed. + + + Source '{0}' is validated. + + + Total package yield:'{0}' for the specified package '{1}'. + + + Type to compare must be an instance of SemanticVersion. + + + The package '{0}' failed to install. + + + Unable to resolve package dependency -- package '{0}' not found. + + + '{0}':Unable to resolve package source '{1}'. + + + The file extension '{0}'::'{1}' is not valid. The required file extension is '.nuspec' or '.nupkg'. + + + Uninstalled '{0}' package. + + + Unknown category for '{0}'::'{1}': '{2}' + + + Uri Scheme '{0}' is not supported. + + + Use the default configuration. + + + '{0}' is '{1}'. + + + The parameter Name '{0}' is required and cannot contain wildcard characters when RequiredVersion, MinimumVersion or MaximumVersion is specified in the same command. + 0 - name + + + Yielding. + + + Yielding the package source: '{0}'. + + + Number of bytes read: '{0}' + 0 - a number + + + Calling Msi provider to install the package '{0}' + + + '{0}': PackageManagement manifest file '{1}' does not exist in '{2}'. + + + '{0}': cannot find packages with a canonicalId = '{1}'. + + + Cannot identify the PackageManagement version. Try 'Get-Module -Name PackageManagement -ListAvailable' to check if the PackageManagement is available. + + + The dependency package '{0}' is already installed. + + + The dependency package '{0}' is not installed. + + + '{0}': Directory '{1}' does exist. + + + Downloading a package '{0}' ... + 0 - package name + + + Downloaded {0:0.00} MB out of {1:0.00} MB. + + + '{0}': Failed to download a package from '{0}' and save it to '{1}'. Try re-run the cmdlet with -verbose -debug to get more information. + 0 - package url; 1- file path + + + '{0}': failed to get a package from the given fast reference '{1}. + 0 - package provider name, 1 - path + + + '{0}': found more than one package ('{1}') with a canonicalId = '{2}'. Please double check if package name, version, and source location are correct. + + + Failed to install the package '{0}' , see error details: '{1}'. + + + Installing ... + + + Installing a package from '{0}' + + + Provider '{0}' is not used to perform this operation because it requires a minimum version of the PackageManagement module to be '{1}', but the current version of the PackageManagement module is '{2}'. + + + '{0}' packages returned by the provider '{1}' for the '{2}' operation. + + + Retry downloading '{0}' for '{1}' more times + + + Running command '{0}' + + + Uninstall operation failed by running a command '{0}' , see error details: '{1}'. + + + UnInstalling a package ... + + + '{0}': uninstalling package '{1}'. + + + Package '{0}' with Source '{1}' has unknown media type '{2}'. + + + The provider '{0}' does not support the Uri format: '{1}'. + + + Unsupported package source '{0}'. + + + '{0}': provider type '{0}' not supported. + + + '{0}' does not support uninstalling '{1}'. Try use -providerName msi. + + + Cannot convert type with more than 2 generic arguments + + + Cannot convert object of type '{0}' to primitive, string or PSObject type. + + + Package List Source '{0}' has incorrect format. + + \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Api/ICoreApi.cs b/src/Microsoft.PackageManagement/Api/ICoreApi.cs new file mode 100644 index 000000000..4bb213852 --- /dev/null +++ b/src/Microsoft.PackageManagement/Api/ICoreApi.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Api { + + /// + /// Provides access to CORE functionality to PROVIDERs as part of the current request object. + /// + public interface ICoreApi { + /// + /// Returns a reference to the PackageManagementService object. + /// + IPackageManagementService PackageManagementService { get; } + + /// Returns a reference to the Provider Services object. + IProviderServices ProviderServices { get; } + } +} diff --git a/src/Microsoft.PackageManagement/Api/IHostAPI.cs b/src/Microsoft.PackageManagement/Api/IHostAPI.cs new file mode 100644 index 000000000..07581b089 --- /dev/null +++ b/src/Microsoft.PackageManagement/Api/IHostAPI.cs @@ -0,0 +1,252 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Api { + using System.Collections.Generic; + using System.Security; + using System.Net; + + /// + /// Functions implemented by the HOST to provide contexual information and control to for the current request. + /// + public interface IHostApi { + + /// + /// The HOST should return true if the current request in progress should be cancelled. + /// + bool IsCanceled {get;} + + /// + /// the HOST should return a localized string for the given messageText, or null if not localizable. + /// + /// + /// The message ID or text string to resolve. + /// + /// + /// a default message text that would be used if there is no match in local resources. + /// This provides the HOST the opportunity to reformat the actual message even if they don't match it. + /// (PSGet uses this) + /// + /// + string GetMessageString(string messageText, string defaultText); + + /// + /// Sends a formatted warning message to the HOST. + /// + /// + /// The fully formatted warning message to display to the user + /// + /// + /// This should return true if the request is cancelled. + /// + bool Warning(string messageText); + + /// + /// Sends a complex Error message to the HOST. + /// + /// An identifier that can be used to uniquely id the Error + /// A category for the message (should map to a PowerShell ErrorCategory enumeration) + /// a target object the operation has failed for. + /// + /// The fully formatted error message to display to the user + /// + /// + /// This should return true if the request is cancelled. + /// /// + bool Error(string id, string category, string targetObjectValue, string messageText); + + /// + /// Sends a status message to the HOST + /// + /// + /// The fully formatted message to display to the user + /// + /// + /// This should return true if the request is cancelled. + /// + bool Message(string messageText); + + /// + /// Sends a message to the verbose channel. + /// + /// + /// The fully formatted verbose message to display to the user + /// + /// + /// This should return true if the request is cancelled. + /// + bool Verbose(string messageText); + + /// + /// Sends a message to the debug channel + /// + /// + /// The fully formatted debug message to display to the user + /// + /// + /// This should return true if the request is cancelled. + /// + bool Debug(string messageText); + + /// + /// Starts a progress indicator + /// + /// the number of a parent progress indicator. Should be zero if there is no parent. + /// + /// The fully formatted progress message to display to the user + /// + /// + /// The progress indicator handle for the new progress message + /// + int StartProgress(int parentActivityId, string messageText); + + /// + /// Write progress using powershell write progress directly + /// + /// Specifies the first line of text in the heading above the status bar. + /// Corresponds to status on write-progress. Specifies the second line of text in the heading above the status bar. This text describes current state of the activity. + /// + /// Corresponds to id on write-progress. Specifies an ID that distinguishes each progress bar from the others. + /// Use this parameter when you are creating more than one progress bar in a single command. + /// If the progress bars do not have different IDs, they are superimposed instead of being displayed in a series. + /// + /// Specifies the percentage of the activity that is completed. Use the value -1 if the percentage complete is unknown or not applicable. + /// Specifies the projected number of seconds remaining until the activity is completed. Use the value -1 if the number of seconds remaining is unknown or not applicable. + /// Specifies the line of text below the progress bar. This text describes the operation that is currently taking place. + /// Identifies the parent activity of the current activity. Use the value -1 if the current activity has no parent activity. + /// Indicates whether the progress bar is visible + /// + bool Progress(string activity, string messageText, int activityId, int progressPercentage, int secondsRemaining, string currentOperation, int parentActivityId, bool completed); + + /// + /// Sends a progress update + /// + /// + /// The Progress indicator ID (from StartProgress) + /// + /// + /// The Percentage for the progress (0-100) + /// + /// + /// The fully formatted progress message to display to the user + /// + /// + /// This should return true if the request is cancelled. + /// + bool Progress(int activityId, int progressPercentage, string messageText); + + /// + /// Ends a progress notification + /// + /// + /// The Progress indicator ID (from StartProgress) + /// + /// + /// true if the operation was successful. + /// + /// + /// This should return true if the request is cancelled. + /// + bool CompleteProgress(int activityId, bool isSuccessful); + + /// + /// Used by a provider to request what metadata keys were passed from the user + /// + /// an collection of the keys for the specified dynamic options + IEnumerable OptionKeys {get;} + + /// + /// Used by a provider to request the values for a given dynamic option + /// + /// the dynamic option Key (should be present in OptionKeys) + /// an collection of the value for the specified dynamic option + IEnumerable GetOptionValues(string key); + + /// + /// Proxy used by provider + /// + IWebProxy WebProxy { get; } + + /// + /// A collection of sources specified by the user. If this is null or empty, the provider should assume 'all the registered sources' + /// + IEnumerable Sources {get;} + + /// + /// A credential username specified by the user + /// + string CredentialUsername {get;} + + /// + /// A credential password specified by the user + /// + SecureString CredentialPassword {get;} + + /// + /// The CORE may ask the HOST if a given provider should be bootstrapped during an operation. + /// + /// the name of the provider or component requesting the provider. + /// the name of the requested provider + /// the miniumum version of the provider required + /// + /// the remote location that the provider is being bootstrapped from + /// the target folder where the provider is to be installed. + /// + bool ShouldBootstrapProvider(string requestor, string providerName, string providerVersion, string providerType, string location, string destination); + + /// + /// the CORE may aske the user if a given package should be allowed to install. + /// + /// the name of the package + /// the name of the source of the package + /// + bool ShouldContinueWithUntrustedPackageSource(string package, string packageSource); + + /// + /// Allow a package provider to comfirm a user whether the process should continue + /// + /// Query that inquires whether the cmdlet should continue. + /// Caption of the window that might be displayed when the user is prompted whether or not to perform the action. + /// True if and only if the user selects the yesToall option. If this is already True, ShouldContinue will bypass the prompt and return True. + /// True if and only if the user selects the noToall option. If this is already True, ShouldContinue will bypass the prompt and return False. + /// + bool ShouldContinue(string query, string caption, ref bool yesToAll, ref bool noToAll); + + /// + /// Allow a package provider to comfirm a user whether the process should continue + /// + /// Query that inquires whether the cmdlet should continue. + /// Caption of the window that might be displayed when the user is prompted whether or not to perform the action. + /// + bool ShouldContinue(string query, string caption); + + /// + /// Asks an arbitrary true/false question of the user. + /// + /// + /// + bool AskPermission(string permission); + + /// + /// The HOST should return 'True' if the current operation is executed in an interactive environment + /// and the user should be able to respond to queries. + /// + bool IsInteractive {get;} + + /// + /// The HOST should give each individual request a unique value (used to track if a particular operation has been tried before) + /// + int CallCount {get;} + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Api/IProviderServices.cs b/src/Microsoft.PackageManagement/Api/IProviderServices.cs new file mode 100644 index 000000000..18f530720 --- /dev/null +++ b/src/Microsoft.PackageManagement/Api/IProviderServices.cs @@ -0,0 +1,53 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Api { + using System; + using System.Collections.Generic; + using PackageManagement.Packaging; + + public interface IProviderServices { + #region declare service-apis + + bool IsElevated {get;} + + IEnumerable FindPackageByCanonicalId(string canonicalId, IRequest requestObject); + + string GetCanonicalPackageId(string providerName, string packageName, string version, string source); + + string ParseProviderName(string canonicalPackageId); + + string ParsePackageName(string canonicalPackageId); + + string ParsePackageVersion(string canonicalPackageId); + + string ParsePackageSource(string canonicalPackageId); + + string DownloadFile(Uri remoteLocation, string localFilename, IRequest requestObject); + + string DownloadFile(Uri remoteLocation, string localFilename,int timeoutMilliseconds, bool showProgress, IRequest requestObject); + + bool IsSupportedArchive(string localFilename, IRequest requestObject); + + IEnumerable UnpackArchive(string localFilename, string destinationFolder, IRequest requestObject); + + bool Install(string fileName, string additionalArgs, IRequest requestObject); + + bool IsSignedAndTrusted(string filename, IRequest requestObject); + + int StartProcess(string filename, string arguments, bool requiresElevation, out string standardOutput, IRequest requestObject); + + #endregion + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Api/IRequest.cs b/src/Microsoft.PackageManagement/Api/IRequest.cs new file mode 100644 index 000000000..ebf237a21 --- /dev/null +++ b/src/Microsoft.PackageManagement/Api/IRequest.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Api { + public interface IRequest : IHostApi, ICoreApi, IResponseApi { + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Api/IResponseApi.cs b/src/Microsoft.PackageManagement/Api/IResponseApi.cs new file mode 100644 index 000000000..c286b9c8c --- /dev/null +++ b/src/Microsoft.PackageManagement/Api/IResponseApi.cs @@ -0,0 +1,222 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Api { + using System; + + /// + /// These functions are implemented by the CORE and passed to the PROVIDER so data can be returned from a given call. + /// + public interface IResponseApi { + /// + /// Used by a provider to return fields for a SoftwareIdentity. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + string YieldSoftwareIdentity(string fastPath, string name, string version, string versionScheme, string summary, string source, string searchKey, string fullPath, string packageFileName); + + /// + /// Used by a provider to return a swidtag in the form of an xml + /// Provider has the option to commit immediately + /// + /// + /// + /// + string YieldSoftwareIdentityXml(string xmlSwidTag, bool commitImmediately); + + /// + /// Adds a tagId to a SoftwareIdentity object + /// + /// + /// + string AddTagId(string tagId); + + /// + /// Adds a xml language attribute to a SoftwareIdentity object + /// + /// + /// + string AddCulture(string culture); + + /// + /// Adds an arbitrary key/value pair of metadata to a SoftwareIdentity + /// + /// This adds the metadata to the first Meta element in the swidtag. + /// + /// + /// + /// a string to a Meta element path. If null, this function did not succeed. + string AddMetadata(string name, string value); + + /// + /// Adds arbitrary key/value pair of metadata to a specific element in a Swidtag. + /// + /// + /// + /// + /// a string to a Meta element path. If null, this function did not succeed. + string AddMetadata(string elementPath, string name, string value); + + /// + /// Adds arbitrary key/value pair of metadata to a specific element in a Swidtag, using a specific namespace. + /// + /// + /// + /// + /// + /// a string to a Meta element path. If null, this function did not succeed. + string AddMetadata(string elementPath, Uri @namespace, string name, string value); + + /// + /// Adds a new Meta Element to the Swidtag. + /// + /// + /// a string to the newly created Meta element. If null, this function did not succeed. + string AddMeta(string elementPath); + + /// + /// Adds a new Entity element to the Swidtag + /// + /// + /// + /// + /// + /// a string to a Entity element path. If null, this function did not succeed. + string AddEntity(string name, string regid, string role, string thumbprint); + + /// + /// Adds a new Link element to the swidtag + /// + /// + /// + /// + /// + /// + /// + /// + /// a string to a Link element path. If null, this function did not succeed. + string AddLink(Uri referenceUri, string relationship, string mediaType, string ownership, string use, string appliesToMedia, string artifact); + + /// + /// Adds a new Dependency (ie, Link, rel="requires") to the swidtag + /// + /// + /// + /// + /// + /// + /// a string to a Link element path. If null, this function did not succeed. + string AddDependency(string providerName, string packageName, string version, string source, string appliesTo); + + + /// + /// Adds a Payload element to the Swidtag + /// + /// a string to a new (or the existing one) Payload element + string AddPayload(); + + /// + /// Adds an Evidence element to the Swidtag + /// + /// + /// + /// a string to a new (or the existing one) Evidence element + string AddEvidence(DateTime date, string deviceId); + + /// + /// Adds a Directory element to a Payload, Evidence or Directory element + /// + /// + /// + /// + /// + /// + /// a string to a new Directory element. If null, this function did not succeed. + string AddDirectory(string elementPath, string directoryName, string location, string root, bool isKey); + + /// + /// Adds a File element to a Payload, Evidence or Directory element + /// + /// + /// + /// + /// + /// + /// + /// + /// a string to a new File element. If null, this function did not succeed. + string AddFile(string elementPath, string fileName, string location, string root, bool isKey, long size, string version); + + /// + /// Adds a Process element to a Payload or Evidence element + /// + /// + /// + /// + /// a string to a new Process element. If null, this function did not succeed. + string AddProcess(string elementPath, string processName, int pid); + + /// + /// Adds a Resource element to a Payload or Evidence element + /// + /// + /// + /// a string to a new Resource element. If null, this function did not succeed. + string AddResource(string elementPath, string type); + + /// + /// Used by a provider to return fields for a package source (repository) + /// + /// + /// + /// + /// + /// + /// A boolean indicating the IsCanceled state. (if the result is false, the provider should exit quickly) + bool YieldPackageSource(string name, string location, bool isTrusted, bool isRegistered, bool isValidated); + + /// + /// Used by a provider to return the fields for a Metadata Definition + /// The cmdlets can use this to supply tab-completion for metadata to the user. + /// + /// the provider-defined name of the option + /// one of ['string','int','path','switch'] + /// if the parameter is mandatory + /// A boolean indicating the IsCanceled state. (if the result is false, the provider should exit quickly) + bool YieldDynamicOption(string name, string expectedType, bool isRequired); + + /// + /// Yields a key/value pair + /// + /// + /// + /// A boolean indicating the IsCanceled state. (if the result is false, the provider should exit quickly) + bool YieldKeyValuePair(string key, string value); + + /// + /// Yields a value + /// + /// + /// A boolean indicating the IsCanceled state. (if the result is false, the provider should exit quickly) + bool YieldValue(string value); + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Constants.cs b/src/Microsoft.PackageManagement/Constants.cs new file mode 100644 index 000000000..fe7719ef3 --- /dev/null +++ b/src/Microsoft.PackageManagement/Constants.cs @@ -0,0 +1,144 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal { + using System; + + internal static class Constants { + internal static readonly string[] SupportedAssemblyTypes = {".dll", ".exe", ".psm1"}; + internal const int PackageManagementVersion = 3; + internal const int TimeoutUnspecified = 0; + internal const int TimeoutHostNotAvailable = -1; + + internal const int HostNotAvailableTimeout = 5*60; // time when host not around + internal const int HostNotAvailableResponsiveness = 5; // five seconds + + internal const int TimeoutAfterCancel = 60; + internal const int ResponsivenessAfterCancel = 1; + internal static TimeSpan DefaultCallTimeout = TimeSpan.FromMinutes(60); + // TODO: Setting responsiveness to 15 minutes until we know that + // we're handling it all right. + internal static TimeSpan DefaultResponsiveness = TimeSpan.FromSeconds(15 * 60); + + internal static TimeSpan Zero = new TimeSpan(0); + internal static string BootstrapNuGet = "BootstrapNuGet"; + + #region declare common-constants-implementation + + /* Synced/Generated code =================================================== */ + + internal const string MinVersion = "0.0.0.1"; + internal const string MSGPrefix = "MSG:"; + internal static string[] Empty = new string[0]; + + internal static class Features { + internal const string AutomationOnly = "automation-only"; + internal const string MagicSignatures = "magic-signatures"; + internal const string SupportedExtensions = "file-extensions"; + internal const string SupportedSchemes = "uri-schemes"; + internal const string SupportsPowerShellModules = "supports-powershell-modules"; + internal const string SupportsRegexSearch = "supports-regex-search"; + internal const string SupportsSubstringSearch = "supports-substring-search"; + internal const string SupportsWildcardSearch = "supports-wildcard-search"; + } + + internal static class Messages { + internal const string CreatefolderFailed = "MSG:CreatefolderFailed"; + internal const string DependencyResolutionError = "MSG:UnableToResolveDependency_dependencyPackage"; + internal const string DependentPackageFailedInstall = "MSG:DependentPackageFailedInstall_dependency"; + internal const string DestinationPathNotSet = "MSG:DestinationPathNotSet"; + internal const string FailedProviderBootstrap = "MSG:FailedProviderBootstrap"; + internal const string ProviderNotResponsive = "MSG:ProviderNotResponsive"; + internal const string ProviderTimeoutExceeded = "MSG:ProviderTimeoutExceeded"; + internal const string FailedPowerShellMetaProvider = "MSG:FailedPowerShellMetaProvider"; + internal const string FileFailedVerification = "MSG:FileFailedVerification"; + internal const string HashNotEqual = "MSG:HashNotEqual"; + internal const string MissingFileTag = "MSG:MissingFileTag"; + internal const string MissingHashAttribute = "MSG:MissingHashAttribute"; + internal const string MissingHashContent = "MSG:MissingHashContent"; + internal const string UnsupportedHashAlgorithm = "MSG:UnsupportedHashAlgorithm"; + internal const string InvalidHashFormat = "MSG:InvalidHashFormat"; + internal const string InvalidFilename = "MSG:InvalidFilename"; + internal const string MissingRequiredParameter = "MSG:MissingRequiredParameter"; + internal const string NetworkNotAvailable = "MSG:NetworkNotAvailable"; + internal const string PackageFailedInstall = "MSG:UnableToInstallPackage_package_reason"; + internal const string PackageFailedInstallErrorLog = "MSG:PackageFailedInstallErrorLog"; + internal const string PackageSourceExists = "MSG:PackageSourceExists"; + internal const string ProtocolNotSupported = "MSG:ProtocolNotSupported"; + internal const string ProviderPluginLoadFailure = "MSG:ProviderPluginLoadFailure"; + internal const string ProviderSwidtagUnavailable = "MSG:ProviderSwidtagUnavailable"; + internal const string RemoveEnvironmentVariableRequiresElevation = "MSG:RemoveEnvironmentVariableRequiresElevation"; + internal const string SchemeNotSupported = "MSG:SchemeNotSupported"; + internal const string SourceLocationNotValid = "MSG:SourceLocationNotValid_Location"; + internal const string UnableToCopyFileTo = "MSG:UnableToCopyFileTo"; + internal const string UnableToCreateShortcutTargetDoesNotExist = "MSG:UnableToCreateShortcutTargetDoesNotExist"; + internal const string UnableToDownload = "MSG:UnableToDownload"; + internal const string UnableToOverwriteExistingFile = "MSG:UnableToOverwriteExistingFile"; + internal const string UnableToRemoveFile = "MSG:UnableToRemoveFile"; + internal const string UnableToResolvePackage = "MSG:UnableToResolvePackage"; + internal const string UnableToResolveSource = "MSG:UnableToResolveSource_NameOrLocation"; + internal const string UnableToUninstallPackage = "MSG:UnableToUninstallPackage"; + internal const string UnknownFolderId = "MSG:UnknownFolderId"; + internal const string UnknownProvider = "MSG:UnknownProvider"; + internal const string ProviderNameAndVersionNotAvailableFromFilePath = "MSG:ProviderNameAndVersionNotAvailableFromFilePath"; + internal const string SingleAssemblyAllowed = "MSG:SingleAssemblyAllowed"; + internal const string UnknownProviderFromActivatedList = "MSG:UnknownProviderFromActivatedList"; + internal const string UnsupportedArchive = "MSG:UnsupportedArchive"; + internal const string UnsupportedProviderType = "MSG:UnsupportedProviderType"; + internal const string UriSchemeNotSupported = "MSG:UriSchemeNotSupported_Scheme"; + internal const string UserDeclinedUntrustedPackageInstall = "MSG:UserDeclinedUntrustedPackageInstall"; + internal const string UnableToFindPowerShellFunctionsFile = "MSG:UnableToFindPowerShellFunctionsFile"; + internal const string FileNotFound = "MSG:FileNotFound"; + internal const string InvalidFileType = "MSG:InvalidFileType"; + internal const string ProviderNameIsNullOrEmpty = "MSG:ProviderNameIsNullOrEmpty"; + internal const string FailedToImportProvider = "MSG:FailedToImportProvider"; + internal const string ModuleNotFound = "MSG:ModuleNotFound"; + internal const string NoMatchFoundForCriteria = "MSG:NoMatchFoundForCriteria"; + internal const string InvalidParameter = "MSG:InvalidParameter"; + internal const string InstallRequiresCurrentUserScopeParameterForNonAdminUser = "MSG:InstallRequiresCurrentUserScopeParameterForNonAdminUser"; + + } + + internal static class Status { + internal const string TimedOut = "TimedOut"; + } + + internal static class PackageStatus { + internal const string Available = "Available"; + internal const string Dependency = "Dependency"; + internal const string Installed = "Installed"; + internal const string Uninstalled = "Uninstalled"; + internal const string Downloaded = "Downloaded"; + } + + internal static class Parameters { + internal const string IsUpdate = "IsUpdatePackageSource"; + internal const string Name = "Name"; + internal const string Location = "Location"; + } + + internal static class Signatures { + internal const string Cab = "4D534346"; + internal const string OleCompoundDocument = "D0CF11E0A1B11AE1"; + internal const string Zip = "504b0304"; + internal static string[] ZipVariants = new[] {Zip, /* should have EXEs? */}; + } + + internal static class SwidTag { + internal const string SoftwareIdentity = "SoftwareIdentity"; + } + + #endregion + } +} diff --git a/src/Microsoft.PackageManagement/ErrorCategory.cs b/src/Microsoft.PackageManagement/ErrorCategory.cs new file mode 100644 index 000000000..5711340f2 --- /dev/null +++ b/src/Microsoft.PackageManagement/ErrorCategory.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal { + public enum ErrorCategory { + NotSpecified, + OpenError, + CloseError, + DeviceError, + DeadlockDetected, + InvalidArgument, + InvalidData, + InvalidOperation, + InvalidResult, + InvalidType, + MetadataError, + NotImplemented, + NotInstalled, + ObjectNotFound, + OperationStopped, + OperationTimeout, + SyntaxError, + ParserError, + PermissionDenied, + ResourceBusy, + ResourceExists, + ResourceUnavailable, + ReadError, + WriteError, + FromStdErr, + SecurityError, + ProtocolError, + ConnectionError, + AuthenticationError, + LimitsExceeded, + QuotaExceeded, + NotEnabled, + } +} diff --git a/src/Microsoft.PackageManagement/IPackageManagementService.cs b/src/Microsoft.PackageManagement/IPackageManagementService.cs new file mode 100644 index 000000000..b730da3e4 --- /dev/null +++ b/src/Microsoft.PackageManagement/IPackageManagementService.cs @@ -0,0 +1,57 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal { + using System; + using System.Collections.Generic; + using Api; + using Implementation; + using PackageManagement.Implementation; + using PackageManagement.Packaging; + + /// + /// The current Package Management Service Interface + /// Binding directly to this is discouraged, as the interface is expected to be incrementally + /// expanded over time. + /// In order to access the interface, the Host (client) is encouraged to copy this interface + /// into their own project and use the PackageManagementService.GetInstance]]> + /// method to dynamically generate a matching implementation at load time. + /// + public interface IPackageManagementService { + int Version {get;} + + IEnumerable ProviderNames {get;} + + IEnumerable AllProviderNames { get; } + + IEnumerable PackageProviders {get;} + + IEnumerable GetAvailableProviders(IHostApi requestObject, string[] names); + + IEnumerable ImportPackageProvider(IHostApi requestObject, string providerName, Version requiredVersion, + Version minimumVersion, Version maximumVersion, bool isRooted, bool force); + + bool Initialize(IHostApi requestObject); + + IEnumerable SelectProvidersWithFeature(string featureName); + + IEnumerable SelectProvidersWithFeature(string featureName, string value); + + IEnumerable SelectProviders(string providerName, IHostApi requestObject); + + IEnumerable FindPackageByCanonicalId(string packageId, IHostApi requestObject); + + bool RequirePackageProvider(string requestor, string packageProviderName, string minimumVersion, IHostApi requestObject); + } +} diff --git a/src/Microsoft.PackageManagement/Implementation/ActionRequestObject.cs b/src/Microsoft.PackageManagement/Implementation/ActionRequestObject.cs new file mode 100644 index 000000000..6eb56bc72 --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/ActionRequestObject.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Implementation { + using System; + using Internal.Api; + + public class ActionRequestObject : RequestObject { + public ActionRequestObject(ProviderBase provider, IHostApi hostApi, Action action) : base(provider, hostApi, action) { + InvokeImpl(); + } + } +} diff --git a/src/Microsoft.PackageManagement/Implementation/Archiver.cs b/src/Microsoft.PackageManagement/Implementation/Archiver.cs new file mode 100644 index 000000000..e373fcc12 --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/Archiver.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Implementation { + using System.Collections.Generic; + using Api; + using Providers; + + internal class Archiver : ProviderBase { + private string _name; + + internal Archiver(IArchiver provider) + : base(provider) { + } + + public override string ProviderName { + get { + return _name ?? (_name = Provider.GetArchiverName()); + } + } + + public override bool IsSupportedFile(string localFilename) { + return base.IsSupportedFile(localFilename) && Provider.IsSupportedFile(localFilename); + } + + public IEnumerable UnpackArchive(string localFilename, string destinationFolder, IHostApi host) { + return new FuncRequestObject>(this, host, (request) => Provider.UnpackArchive(localFilename, destinationFolder, request)).Value; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Implementation/DefaultPackageProvider.cs b/src/Microsoft.PackageManagement/Implementation/DefaultPackageProvider.cs new file mode 100644 index 000000000..f8655fe6f --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/DefaultPackageProvider.cs @@ -0,0 +1,127 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +namespace Microsoft.PackageManagement.Internal.Implementation +{ + using System; + using PackageManagement.Internal.Packaging; + using Providers; + + /// + /// This DefaultPackageProvider type is mainly used for the PowerShell console output. + /// + public class DefaultPackageProvider : Swidtag, IPackageProvider { + + private readonly string _providerName; + private readonly string _providerVersion; + + public DefaultPackageProvider(string name, string version) { + _providerVersion = version; + _providerName = name; + } + + public void InitializeProvider(Api.IRequest requestObject) { + if (requestObject == null) { + throw new ArgumentNullException("requestObject"); + } + + requestObject.Debug("Called DummyProvider InitializeProvider"); + } + + internal bool IsLoaded {set; get;} + + public string ProviderPath {set; get;} + + + public string GetPackageProviderName() { + return _providerName; + } + + public string GetProviderVersion() { + return _providerVersion; + } + + public void AddPackageSource(string name, string location, bool trusted, Api.IRequest requestObject) { + throw new NotImplementedException(); + } + + public void ResolvePackageSources(Api.IRequest requestObject) { + if (requestObject == null) { + throw new ArgumentNullException("requestObject"); + } + + requestObject.Debug("Called DummyProvider ResolvePackageSources"); + } + + public void GetDynamicOptions(string category, Api.IRequest requestObject) { + if (requestObject == null) { + throw new ArgumentNullException("requestObject"); + } + + requestObject.Debug("Called DummyProvider GetDynamicOptions"); + } + + public void RemovePackageSource(string source, Api.IRequest requestObject) { + throw new NotImplementedException(); + } + + public void FindPackage(string name, string requiredVersion, string minimumVersion, string maximumVersion, int id, Api.IRequest requestObject) { + throw new NotImplementedException(); + } + + public void FindPackageByFile(string filePath, int id, Api.IRequest requestObject) { + throw new NotImplementedException(); + } + + public void FindPackageByUri(Uri uri, int id, Api.IRequest requestObject) { + throw new NotImplementedException(); + } + + public void GetInstalledPackages(string name, string requiredVersion, string minimumVersion, string maximumVersion, Api.IRequest requestObject) { + throw new NotImplementedException(); + } + + public void DownloadPackage(string fastPath, string location, Api.IRequest requestObject) { + throw new NotImplementedException(); + } + + public void GetPackageDetails(string fastPath, Api.IRequest requestObject) { + throw new NotImplementedException(); + } + + public void InstallPackage(string fastPath, Api.IRequest requestObject) { + throw new NotImplementedException(); + } + + public void UninstallPackage(string fastPath, Api.IRequest requestObject) { + throw new NotImplementedException(); + } + + + public void GetFeatures(Api.IRequest requestObject) { + throw new NotImplementedException(); + } + + public bool IsMethodImplemented(string methodName) { + throw new NotImplementedException(); + } + + public void OnUnhandledException(string methodName, Exception exception) { + throw new NotImplementedException(); + } + } +} + + diff --git a/src/Microsoft.PackageManagement/Implementation/DictionaryRequestObject.cs b/src/Microsoft.PackageManagement/Implementation/DictionaryRequestObject.cs new file mode 100644 index 000000000..bcfedb174 --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/DictionaryRequestObject.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Implementation { + using System; + using System.Collections.Generic; + using Api; + using Utility.Async; + using Utility.Extensions; + + public class DictionaryRequestObject : RequestObject, IAsyncValue>> { + private readonly Dictionary> _results = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + public DictionaryRequestObject(ProviderBase provider, IHostApi request, Action action) + : base(provider, request, action) { + InvokeImpl(); + } + + public Dictionary> Value { + get { + this.Wait(); + return _results; + } + } + + public override bool YieldKeyValuePair(string key, string value) { + _results.GetOrAdd(key, () => new List()).Add(value); + return !IsCanceled; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Implementation/Downloader.cs b/src/Microsoft.PackageManagement/Implementation/Downloader.cs new file mode 100644 index 000000000..eeb4a4f98 --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/Downloader.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Implementation { + using System; + using Api; + using Providers; + + internal class Downloader : ProviderBase { + private string _name; + + internal Downloader(IDownloader provider) + : base(provider) { + } + + public override string ProviderName { + get { + return _name ?? (_name = Provider.GetDownloaderName()); + } + } + + public string DownloadFile(Uri remoteLocation, string localFilename, int timeoutMilliseconds, bool showProgress, IHostApi requestObject) { + return new FuncRequestObject(this, requestObject, request => Provider.DownloadFile(remoteLocation, localFilename,timeoutMilliseconds,showProgress, request)).Value; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Implementation/DynamicOptionRequestObject.cs b/src/Microsoft.PackageManagement/Implementation/DynamicOptionRequestObject.cs new file mode 100644 index 000000000..21af88823 --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/DynamicOptionRequestObject.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Implementation { + using System; + using System.Collections.Generic; + using Api; + using Packaging; + + internal class DynamicOptionRequestObject : EnumerableRequestObject { + private DynamicOption _currentItem; + private List _list = new List(); + private readonly OptionCategory _category; + + public DynamicOptionRequestObject(ProviderBase provider, IHostApi request, Action action, OptionCategory category) + : base(provider, request, action) { + _category = category; + InvokeImpl(); + } + + public override bool YieldDynamicOption(string name, string expectedType, bool isRequired) { + Activity(); + + if (_currentItem != null) { + _currentItem.PossibleValues = _list.ToArray(); + _list = new List(); + Results.Add(_currentItem); + } + + OptionType typ; + + if (Enum.TryParse(expectedType, true, out typ)) { + _currentItem = new DynamicOption { + Category = _category, + Name = name, + Type = typ, + IsRequired = isRequired, + ProviderName = Provider.ProviderName, + }; + } + return !IsCanceled; + } + + public override bool YieldKeyValuePair(string key, string value) { + Activity(); + + if (_currentItem != null && _currentItem.Name == key) { + _list.Add(value); + } + return !IsCanceled; + } + + protected override void Complete() { + if (_currentItem != null) { + _currentItem.PossibleValues = _list.ToArray(); + Results.Add(_currentItem); + } + + base.Complete(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Implementation/EnumerableRequestObject.cs b/src/Microsoft.PackageManagement/Implementation/EnumerableRequestObject.cs new file mode 100644 index 000000000..5a4ea7a07 --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/EnumerableRequestObject.cs @@ -0,0 +1,75 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Implementation { + using System; + using System.Collections; + using System.Collections.Generic; + using System.Threading; + using Api; + using Utility.Async; + using Utility.Collections; + + internal class EnumerableRequestObject : RequestObject, IAsyncEnumerable { + protected readonly BlockingCollection Results = new BlockingCollection(); + + internal EnumerableRequestObject(ProviderBase provider, IHostApi request, Action action) + : base(provider, request, action) { + OnCancel += () => {Results.CompleteAdding();}; + OnAbort += () => {Results.CompleteAdding();}; + } + + public IEnumerator GetEnumerator() { + return new CancellableEnumerator(_cancellationTokenSource, Results.GetEnumerator()); + } + + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + + public IEnumerable GetConsumingEnumerable() { + return Results.GetConsumingEnumerable(_cancellationTokenSource.Token); + } + + public IEnumerable GetBlockingEnumerable() { + return Results.GetBlockingEnumerable(_cancellationTokenSource.Token); + } + + public bool IsConsumed { + get { + return IsAborted || IsCanceled || Results.IsCompleted; + } + } + + public bool HasData { + get { + return Results.HasData; + } + } + + public WaitHandle Ready { + get { + return Results.Ready; + } + } + + protected override void Complete() { + // we call base.Complete() before calling Results.CompleteAdding() because if a command like + // find-package xjea, xfirefox is run on nanoserver, only the first package will be found. + // It seems that the operation was cancelled early. + base.Complete(); + Results.CompleteAdding(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Implementation/Extensions.cs b/src/Microsoft.PackageManagement/Implementation/Extensions.cs new file mode 100644 index 000000000..f348ea434 --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/Extensions.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Implementation { + using System.Globalization; + using Api; + using Utility.Extensions; + + internal static class Extensions { + internal static string FormatMessageString(this IHostApi request, string messageText, params object[] args) { + if (string.IsNullOrWhiteSpace(messageText)) { + return string.Empty; + } + + if (messageText.IndexOf(Constants.MSGPrefix, System.StringComparison.CurrentCultureIgnoreCase) == 0) { + messageText = request.GetMessageString(messageText.Substring(Constants.MSGPrefix.Length), messageText) ?? messageText; + } + + return args == null || args.Length == 0 ? messageText : messageText.format(args); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Implementation/FuncRequestObject.cs b/src/Microsoft.PackageManagement/Implementation/FuncRequestObject.cs new file mode 100644 index 000000000..aeedd0390 --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/FuncRequestObject.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Implementation { + using System; + using Api; + using Utility.Async; + + public class FuncRequestObject : RequestObject, IAsyncValue { + private T _result; + + public FuncRequestObject(ProviderBase provider, IHostApi hostApi, Func function) + : base(provider, hostApi, null) { + _action = r => {_result = function(r);}; + InvokeImpl(); + } + + public T Value { + get { + // wait for end. + this.Wait(); + return _result; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Implementation/PackageDetailsRequestObject.cs b/src/Microsoft.PackageManagement/Implementation/PackageDetailsRequestObject.cs new file mode 100644 index 000000000..665921215 --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/PackageDetailsRequestObject.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Implementation { + using System; + using Api; + using PackageManagement.Packaging; + + public class PackageDetailsRequestObject : RequestObject { + private SoftwareIdentity _softwareIdentity; + + public PackageDetailsRequestObject(ProviderBase provider, IHostApi hostApi, SoftwareIdentity softwareIdentity, Action action) + : base(provider, hostApi, action) { + _softwareIdentity = softwareIdentity; + InvokeImpl(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Implementation/PackageManagementService.cs b/src/Microsoft.PackageManagement/Implementation/PackageManagementService.cs new file mode 100644 index 000000000..e4fc27bf8 --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/PackageManagementService.cs @@ -0,0 +1,1614 @@ + +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Implementation { + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Security.AccessControl; + using Api; + using PackageManagement.Implementation; + using PackageManagement.Packaging; + using Packaging; + using Providers; + using Utility.Collections; + using Utility.Extensions; + using Utility.Platform; + using Utility.Plugin; + using Utility.Versions; + using Win32; + using Directory = System.IO.Directory; + using File = System.IO.File; +#if CORECLR + using System.Management.Automation; +#endif + + /// + /// The Client API is designed for use by installation hosts: + /// - PackageManagement Powershell Cmdlets + /// The Client API provides high-level consumer functions to support SDII functionality. + /// + internal class PackageManagementService : IPackageManagementService { + + private static int _lastCallCount; + private static HashSet _providersTriedThisCall; + private string[] _bootstrappableProviderNames; + private bool _initialized; + + // well known, built in provider assemblies. + private readonly string[] _defaultProviders = { + Path.GetFullPath(CurrentAssemblyLocation), // load the providers from this assembly + "Microsoft.PackageManagement.MetaProvider.PowerShell.dll" + }; + + private readonly object _lockObject = new object(); + private readonly IDictionary _metaProviders = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly IDictionary _packageProviders = new Dictionary(StringComparer.OrdinalIgnoreCase); + internal readonly IDictionary Archivers = new Dictionary(StringComparer.OrdinalIgnoreCase); + internal readonly IDictionary Downloaders = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> _providerCacheTable = new Dictionary>(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _providerFiles = new Dictionary(StringComparer.OrdinalIgnoreCase); + private string _baseDir; + internal bool InternalPackageManagementInstallOnly = false; + private readonly string _nuget ="NuGet"; + + internal enum ProviderOption + { + AllProvider = 0, + LatestVersion = 1, + } + + internal Dictionary> ProviderCacheTable + { + get + { + return _providerCacheTable; + } + } + + internal static string CurrentAssemblyLocation + { + get + { +#if !CORECLR + return Assembly.GetExecutingAssembly().Location; +#else + return typeof(PackageManagementService).GetTypeInfo().Assembly.ManifestModule.FullyQualifiedName; +#endif + } + } + + internal string BaseDir { + get { + return _baseDir ?? (_baseDir = Path.GetDirectoryName(CurrentAssemblyLocation)); + } + } + + internal string[] BootstrappableProviderNames { + get { + return _bootstrappableProviderNames ?? new string[0]; + } + set { + if (_bootstrappableProviderNames.IsNullOrEmpty()) { + _bootstrappableProviderNames = value; + } + } + } + + internal IEnumerable AutoLoadedAssemblyLocations { + get + { + var folder = Path.GetDirectoryName(CurrentAssemblyLocation); + if (!string.IsNullOrWhiteSpace(folder) && folder.DirectoryExists()) { + yield return folder; + } + } + } + internal IEnumerable ProviderAssembliesLocation { + get { + var folder = SystemAssemblyLocation; + if (!string.IsNullOrWhiteSpace(folder) && folder.DirectoryExists()) { + yield return folder; + } + + folder = UserAssemblyLocation; + if (!string.IsNullOrWhiteSpace(folder) && folder.DirectoryExists()) { + yield return folder; + } + } + } +#if CORECLR + private IEnumerable PowerShellModulePath { + get { + var psModulePath = Environment.GetEnvironmentVariable("PSModulePath") ?? ""; + var paths = psModulePath.Split(new char[] {';'}, StringSplitOptions.RemoveEmptyEntries).ToArray(); + return paths; + } + } +#endif + internal string UserAssemblyLocation { + get { + try { +#if !CORECLR + var basepath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); +#else + var basepath = System.Environment.GetEnvironmentVariable("LocalAppData"); +#endif + if (string.IsNullOrWhiteSpace(basepath)) { + return null; + } + + var path = Path.Combine(basepath, @"PackageManagement\ProviderAssemblies"); + if (!Directory.Exists(path)) { + Directory.CreateDirectory(path); + } + return path; + } catch { + // if it can't be created, it's not the end of the world. + } + return null; + } + } + + internal string SystemAssemblyLocation { + get { + try { +#if !CORECLR + var basepath = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); +#else + var basepath = System.Environment.GetEnvironmentVariable("ProgramFiles"); +#endif + if (string.IsNullOrWhiteSpace(basepath)) { + return null; + } + var path = Path.Combine(basepath, @"PackageManagement\ProviderAssemblies"); + + if (!Directory.Exists(path)) { + Directory.CreateDirectory(path); + } + return path; + } catch { + // ignore non-existant directory for now. + } + return null; + } + } + + public IEnumerable PackageProviders { + get { + return _packageProviders.Values; + } + } + + public bool Initialize(IHostApi request) { + lock (_lockObject) { + if (!_initialized) { + LoadProviders(request); + _initialized = true; + } + } + return _initialized; + } + + public int Version { + get { + return Constants.PackageManagementVersion; + } + } + + public IEnumerable ProviderNames { + get { + return _packageProviders.Keys; + } + } + + public IEnumerable AllProviderNames { + get { + if (BootstrappableProviderNames.IsNullOrEmpty()) { + return _packageProviders.Keys; + } + + return _packageProviders.Where(p => p.Value != null && (p.Value.Features == null || !p.Value.Features.ContainsKey(Constants.Features.AutomationOnly))) + .Select(each => each.Key).Concat(BootstrappableProviderNames).Distinct(StringComparer.OrdinalIgnoreCase); + } + } + + public IEnumerable SelectProvidersWithFeature(string featureName) { + return _packageProviders.Values.Where(each => each.Features.ContainsKey(featureName)); + } + + public IEnumerable SelectProvidersWithFeature(string featureName, string value) { + return _packageProviders.Values.Where(each => each.Features.ContainsKey(featureName) && each.Features[featureName].Contains(value)); + } + + public IEnumerable SelectProviders(string providerName, IHostApi hostApi) { + if (!string.IsNullOrWhiteSpace(providerName)) { + // match with wildcards + var results = _packageProviders.Values.Where(each => each.ProviderName.IsWildcardMatch(providerName)).ReEnumerable(); + if (results.Any()) { + return results; + } + + // If the provider is installed but not imported, let's import it + // we don't want import package provider via name to write errors, because in that case the subsequent call to bootstrapper provider will get cancelled. + var provider = ImportPackageProviderHelper(hostApi, providerName, null, null, null, false, false, false).ToArray(); + if (provider.Any()) { + return provider; + } + + if (hostApi != null && !providerName.ContainsWildcards()) { + // if the end user requested a provider that's not there. perhaps the bootstrap provider can find it. + if (RequirePackageProvider(null, providerName, Constants.MinVersion, hostApi)) { + // seems to think we found it. + if (_packageProviders.ContainsKey(providerName)) { + return _packageProviders[providerName].SingleItemAsEnumerable(); + } + } + + // SelectProviders() is iterating through the loaded provider list. As we still need to go through the + // unloaded provider list, we should not warn users yet at this point of time. + // If the provider is not found, eventually we will error out in SelectProviders()/cmdletbase.cs(). + + //hostApi.Warn(hostApi.FormatMessageString(Constants.Messages.UnknownProvider, providerName)); + } + return Enumerable.Empty(); + } else { + // If a user does not specify -provider or -provider name, we will bootstrap the nuget provider if it does not exist. + //Only find, install, uninstall, and save cmdlets requires the bootstrap. + var bootstrapNuGet = hostApi.GetOptionValues(Constants.BootstrapNuGet).FirstOrDefault(); + + if ((bootstrapNuGet != null) && bootstrapNuGet.EqualsIgnoreCase("true")) { + //check if the NuGet provider is already loaded + if (!_packageProviders.Keys.Any(each => each.EqualsIgnoreCase(_nuget))) { + //we'll bootstrap NuGet provider under the following cases: + //case 1: on a clean VM, type install-package foobar + //case 2: on a existing VM, if the nuget provider does not exist and type install-package foobar + //case 3: An existing VM has a old version of the NuGet installed, no bootstrap will occur. This means there is no changes + // to the user, unless he does 'install-packageprovider -name nuget -force'. + if (RequirePackageProvider(null, _nuget, Constants.MinVersion, hostApi)) { + // seems to think we found it. + if (_packageProviders.ContainsKey(_nuget)) { + return PackageProviders.Concat(_packageProviders[_nuget].SingleItemAsEnumerable()); + } + } + } + } + } + + return PackageProviders; + } + + public IEnumerable FindPackageByCanonicalId(string packageId, IHostApi hostApi) { + Uri pkgId; + if (Uri.TryCreate(packageId, UriKind.Absolute, out pkgId)) { + var segments = pkgId.Segments; + if (segments.Length > 0) { + var provider = SelectProviders(pkgId.Scheme, hostApi).FirstOrDefault(); + if (provider != null) { + var name = Uri.UnescapeDataString(segments[0].Trim('/', '\\')); + var version = (segments.Length > 1) ? Uri.UnescapeDataString(segments[1]) : null; + var source = pkgId.Fragment.TrimStart('#'); + var sources = (string.IsNullOrWhiteSpace(source) ? hostApi.Sources : Uri.UnescapeDataString(source).SingleItemAsEnumerable()).ToArray(); + + var host = new object[] { + new { + GetSources = new Func>(() => sources), + GetOptionValues = new Func>(key => key.EqualsIgnoreCase("FindByCanonicalId") ? new[] {"true"} : hostApi.GetOptionValues(key)), + GetOptionKeys = new Func>(() => hostApi.OptionKeys.ConcatSingleItem("FindByCanonicalId")), + }, + hostApi, + }.As(); + + return provider.FindPackage(name, version, null, null, host).Select(each => { + each.Status = Constants.PackageStatus.Dependency; + return each; + }).ReEnumerable(); + } + } + } + return new SoftwareIdentity[0]; + } + + public bool RequirePackageProvider(string requestor, string packageProviderName, string minimumVersion, IHostApi hostApi) { + // check if the package provider is already installed + if (_packageProviders.ContainsKey(packageProviderName)) { + var current = _packageProviders[packageProviderName].Version; + if (current >= minimumVersion) { + return true; + } + } + + var currentCallCount = hostApi.CallCount; + + if (_lastCallCount >= currentCallCount) { + // we've already been here this call. + + // are they asking for the same provider again? + if (_providersTriedThisCall.Contains(packageProviderName)) { + hostApi.Debug("Skipping RequirePackageProvider -- tried once this call previously."); + return false; + } + // remember this in case we come back again. + _providersTriedThisCall.Add(packageProviderName); + } else { + _lastCallCount = currentCallCount; + _providersTriedThisCall = new HashSet { + packageProviderName + }; + } + + if (!hostApi.IsInteractive) { + hostApi.Debug("Skipping RequirePackageProvider due to not interactive"); + // interactive indicates that the host can respond to queries -- this doesn't happen + // in powershell during tab-completion. + return false; + } + + // no? + // ask the bootstrap provider if there is a package provider with that name available. + if (!_packageProviders.ContainsKey("Bootstrap")) { + return false; + } + + var bootstrap = _packageProviders["Bootstrap"]; + if (bootstrap == null) { + hostApi.Debug("Skipping RequirePackageProvider due to missing bootstrap provider"); + return false; + } + + var pkg = bootstrap.FindPackage(packageProviderName, null, minimumVersion, null, hostApi).OrderByDescending(p => p, SoftwareIdentityVersionComparer.Instance).GroupBy(package => package.Name).ToArray(); + if (pkg.Length == 1) { + // Yeah? Install it. + var package = pkg[0].FirstOrDefault(); + var metaWithProviderType = package.Meta.FirstOrDefault(each => each.ContainsKey("providerType")); + var providerType = metaWithProviderType == null ? "unknown" : metaWithProviderType.GetAttribute("providerType"); + var destination = providerType == "assembly" ? (AdminPrivilege.IsElevated ? SystemAssemblyLocation : UserAssemblyLocation) : string.Empty; + var link = package.Links.FirstOrDefault(each => each.Relationship == "installationmedia"); + var location = string.Empty; + if (link != null) { + location = link.HRef.ToString(); + } + + // what can't find an installationmedia link? + // todo: what should we say here? + if (hostApi.ShouldBootstrapProvider(requestor, package.Name, package.Version, providerType, location, destination)) { + var newRequest = hostApi.Extend(new { + GetOptionValues = new Func>(key => { + if (key == "DestinationPath") { + return new[] { + destination + }; + } + return new string[0]; + }) + }); + var packagesInstalled = bootstrap.InstallPackage(package, newRequest).LastOrDefault(); + if (packagesInstalled == null) { + // that's sad. + hostApi.Error(Constants.Messages.FailedProviderBootstrap, ErrorCategory.InvalidOperation.ToString(), package.Name, hostApi.FormatMessageString(Constants.Messages.FailedProviderBootstrap, package.Name)); + return false; + } + // so it installed something + // we must tell the plugin loader to reload the plugins again. + LoadProviders(hostApi); + return true; + } + } + + return false; + } + + /// + /// Get all available providers. + /// + /// + /// providers to be loaded. + public IEnumerable GetAvailableProviders(IHostApi request, string[] providerNames) { + + //Handling two cases + //1. Both "-Name" and "-Listavailable" exist + //2. "-Listavailable" only. + + return providerNames.IsNullOrEmpty() ? + GetAvailableProvider(request, String.Empty) : + providerNames.SelectMany(each => GetAvailableProvider(request, each)); + } + + /// + /// Get available provider. It handles "Get-Packageprovider -Name -ListAvailable" and "Get-Packageprovider -ListAvailable" + /// + /// + /// Name of the provider to be loaded. + private IEnumerable GetAvailableProvider(IHostApi request, string providerName) { + + //This method is called when get-packageprovider -ListAvailable + //We will return whatever we can find + ScanForAvailableProviders(request, providerName, null, null, null); + + //Check if the provider is in the cache + var packageProviders = GetPackageProviderFromCacheTable(providerName).ReEnumerable(); + + return packageProviders.Any() ? packageProviders.Where(p => p.Features == null || !p.Features.ContainsKey(Constants.Features.AutomationOnly)) + : Enumerable.Empty(); + } + + + private IEnumerable GetPackageProviderFromCacheTable(string providerName) + { + // latest version of the providers will be displayed first + var cacheList = (string.IsNullOrWhiteSpace(providerName)) ? _providerCacheTable.SelectMany(each => each.Value.OrderByDescending(provider => provider.Version)).WhereNotNull() + : _providerCacheTable.Where(each => each.Key.IsWildcardMatch(providerName)).SelectMany(each => each.Value.OrderByDescending(provider => provider.Version)).WhereNotNull(); + + return cacheList; + } + + private void ScanForAvailableProviders(IHostApi request, + string providerName, + Version requiredVersion, + Version minimumVersion, + Version maximumVersion, + bool shouldRefreshCache = false, + bool logWarning = true) { + + ResetProviderCachetable(); + + //search assemblies from the well-known locations and update the internal provider cache table + var providerAssemblies = ScanAllProvidersFromProviderAssembliesLocation(request, providerName, requiredVersion, minimumVersion, maximumVersion, ProviderOption.AllProvider).ToArray(); + + //find out which one are from root directory. Because we cannot tell its version and provider name + //we need to load it. + + var files = providerAssemblies.Where(each => ProviderAssembliesLocation.ContainsIgnoreCase(Path.GetDirectoryName(each))).ReEnumerable(); + + //after the cache table gets cleaned, we need to load these assemblies sitting at the top level folder + files.ParallelForEach(providerAssemblyName => { + lock (_providerFiles) { + if (_providerFiles.ContainsKey(providerAssemblyName)) { + //remove the same file from the _providerFiles if any, so it gets reentered + //to the cache table. + _providerFiles.Remove(providerAssemblyName); + } + } + LoadProviderAssembly(request, providerAssemblyName, false); + }); + + var powerShellMetaProvider = GetMetaProviderObject(request); + if (powerShellMetaProvider == null) { + return; + } + + //Get available powershell providers + powerShellMetaProvider.RefreshProviders(request.As(), providerName, requiredVersion, minimumVersion, maximumVersion, logWarning); + } + + /// + /// Import a package provider. + /// + /// + /// Provider name or file path + /// The provider version to be loaded + /// The minimum version of the provider to be loaded + /// The maximum version of the provider to be loaded + /// Whether the 'providerName' is path or name + /// Whether -force is specified + /// + public IEnumerable ImportPackageProvider(IHostApi request, + string providerName, + Version requiredVersion, + Version minimumVersion, + Version maximumVersion, + bool isPathRooted, + bool force) { + + return ImportPackageProviderHelper(request, providerName, requiredVersion, minimumVersion, maximumVersion, isPathRooted, force, true); + } + + /// + /// Import a package provider. + /// + /// + /// Provider name or file path + /// The provider version to be loaded + /// The minimum version of the provider to be loaded + /// The maximum version of the provider to be loaded + /// Whether the 'providerName' is path or name + /// Whether -force is specified + /// if true then we use write error when there is an + /// error when importing with name + /// + private IEnumerable ImportPackageProviderHelper(IHostApi request, + string providerName, + Version requiredVersion, + Version minimumVersion, + Version maximumVersion, + bool isPathRooted, + bool force, + bool throwErrorWhenImportWithName) { + + request.Debug(string.Format(CultureInfo.CurrentCulture, "Calling ImportPackageProvider. providerName = '{0}', requiredVersion='{1}', minimumVersion = '{2}', maximumVersion='{3}'", + providerName, requiredVersion, minimumVersion, maximumVersion)); + + if (string.IsNullOrWhiteSpace(providerName)) { + return Enumerable.Empty(); + } + if (providerName.ContainsWildcards()) { + request.Error(Constants.Messages.InvalidParameter, ErrorCategory.InvalidData.ToString(), providerName, string.Format(CultureInfo.CurrentCulture, Resources.Messages.InvalidParameter, "Import-PackageProvider")); + return Enumerable.Empty(); + } + + if (isPathRooted) { + + if (!File.Exists(providerName)) { + request.Error(Constants.Messages.InvalidFilename, ErrorCategory.InvalidData.ToString(), providerName, string.Format(CultureInfo.CurrentCulture, Resources.Messages.FileNotFound, providerName)); + return Enumerable.Empty(); + } + + //Check if the file type is supported: .dll, .exe, or .psm1 + if (!Constants.SupportedAssemblyTypes.Any(each => each.EqualsIgnoreCase(Path.GetExtension(providerName)))) { + var fileTypes = Constants.SupportedAssemblyTypes.Aggregate(string.Empty, (current, each) => current + " " + each); + request.Error(Constants.Messages.InvalidFilename, ErrorCategory.InvalidData.ToString(), providerName, string.Format(CultureInfo.CurrentCulture, Resources.Messages.InvalidFileType, providerName, fileTypes)); + return Enumerable.Empty(); + } + } + + var providers = isPathRooted ? ImportPackageProviderViaPath(request, providerName, requiredVersion, minimumVersion, maximumVersion, force) + : ImportPackageProviderViaName(request, providerName, requiredVersion, minimumVersion, maximumVersion, force, throwErrorWhenImportWithName); + + return providers; + } + + private IEnumerable ImportPackageProviderViaPath(IHostApi request, + string providerPath, + Version requiredVersion, + Version minimumVersion, + Version maximumVersion, + bool force) { + request.Debug(string.Format(CultureInfo.CurrentCulture, "Calling ImportPackageProviderViaPath. providerName = '{0}', requiredVersion='{1}', minimumVersion = '{2}', maximumVersion='{3}'", + providerPath, requiredVersion, minimumVersion, maximumVersion)); + + var extension = Path.GetExtension(providerPath); + + if (extension != null && extension.EqualsIgnoreCase(".psm1")) { + //loading the PowerShell provider + request.Verbose(string.Format(CultureInfo.CurrentCulture, Resources.Messages.LoadingPowerShellModule, providerPath)); + return ImportPowerShellProvider(request, providerPath, requiredVersion, force); + } + + //loading non-PowerShell providers + request.Verbose(string.Format(CultureInfo.CurrentCulture, Resources.Messages.LoadingAssembly, providerPath)); + + var loaded = LoadProviderAssembly(request, providerPath, force); + + if (loaded) { + return _packageProviders.Where(p => p.Value.ProviderPath.EqualsIgnoreCase(providerPath)).Select(each => each.Value); + } + + return Enumerable.Empty(); + } + + private IEnumerable ImportPackageProviderViaName(IHostApi request, + string providerName, + Version requiredVersion, + Version minimumVersion, + Version maximumVersion, + bool force, + bool throwErrorWhenImportWithName) { + request.Debug(string.Format(CultureInfo.CurrentCulture, "Calling ImportPackageProviderViaName. providerName = '{0}', requiredVersion='{1}', minimumVersion = '{2}', maximumVersion='{3}'", + providerName, requiredVersion, minimumVersion, maximumVersion)); + + //Check if the module or assembly is already loaded + //key = path, value = version + HashSet> refreshingProvidersPaths = new HashSet>(); + foreach (var provider in _packageProviders) { + if (provider.Key.IsWildcardMatch(providerName)) { + //found the provider with the same name is already loaded + + if (force) { + // if -force is present and required version is specified, we will enforce that the loaded provider version must match the required version + if ((requiredVersion != null && provider.Value.Version == (FourPartVersion)requiredVersion) + // if -force is specified and no version information is provided, we will re-import directly from the path of the loaded provider + ||(requiredVersion == null && maximumVersion == null && minimumVersion == null)) + { + refreshingProvidersPaths.Add(new KeyValuePair(_packageProviders[provider.Key].ProviderPath, _packageProviders[provider.Key].Version)); + } + request.Verbose(string.Format(CultureInfo.CurrentCulture, Resources.Messages.ReImportProvider, provider.Key)); + } else { + request.Verbose(string.Format(CultureInfo.CurrentCulture, Resources.Messages.ProviderImportedAlready, provider.Key)); + return Enumerable.Empty(); + } + } + } + + //reload the assembly + foreach (var providerPath in refreshingProvidersPaths) { + var providers = ImportPackageProviderViaPath(request, providerPath.Key, providerPath.Value, minimumVersion, maximumVersion, force); + return providers; + + } + + IEnumerable results = null; + + // if user doesn't have all the available providers in the cache, + // then there is a chance that we will miss the latest version of the provider + // so we will only try to search from the cache table without refreshing it + // if the user does not provide -force and either maximum or minimum version. + if (!force || (maximumVersion == null && minimumVersion == null)) + { + //check if the provider is in the cache table + results = FindMatchedProvidersFromInternalCacheTable(request, providerName, requiredVersion, minimumVersion, maximumVersion, force).ToArray(); + if (results.Any()) + { + return results; + } + } + + //If the provider is not in the cache list, rescan for providers + ScanForAvailableProviders(request, providerName, requiredVersion, minimumVersion, maximumVersion, true, false); + results = FindMatchedProvidersFromInternalCacheTable(request, providerName, requiredVersion, minimumVersion, maximumVersion, force).ToArray(); + if (!results.Any()) { + if (throwErrorWhenImportWithName) + { + request.Error(Constants.Messages.NoMatchFoundForCriteria, ErrorCategory.InvalidData.ToString(), + providerName, string.Format(CultureInfo.CurrentCulture, Resources.Messages.NoMatchFoundForCriteria, providerName)); + } + else + { + request.Verbose(string.Format(CultureInfo.CurrentCulture, Resources.Messages.NoMatchFoundForCriteria, providerName)); + } + } else { + return results; + } + + return Enumerable.Empty(); + } + + + private IEnumerable FindMatchedProvidersFromInternalCacheTable( + IHostApi request, + string providerName, + Version requiredVersion, + Version minimumVersion, + Version maximumVersion, + bool force) { + + //Search from the internal table to see if we can the matched provider + //check if the provider name matches + var providersTable = _providerCacheTable.Where(each => each.Key.IsWildcardMatch(providerName)) + .Select(each => each.Value).ToArray(); + //check if version matches + var providers = providersTable.Select(list => list.Where(each => { + bool foundMatch = true; + if (requiredVersion != null) { + return each.Version.Equals(requiredVersion); + } + + if (minimumVersion != null) { + foundMatch = each.Version >= (FourPartVersion)minimumVersion; + } + if (maximumVersion != null) { + foundMatch &= each.Version <= (FourPartVersion)maximumVersion; + } + + return foundMatch; + }).Select(each => each)).ToArray(); + + var selectedProviders = providers.Select(each => each.OrderByDescending(p => p.Version).FirstOrDefault()).WhereNotNull(); + + foreach (var instance in selectedProviders) { + if (instance.IsLoaded) { + //Initialize the provider + instance.Initialize(request); + + //Add it to the provider list that all imported and in use + _packageProviders.AddOrSet(instance.ProviderName, instance); + request.Verbose(string.Format(Resources.Messages.ImportPackageProvider, instance.ProviderName)); + yield return instance; + } else { + if (Path.GetExtension(instance.ProviderPath).EqualsIgnoreCase(".psm1")) { + + //it's a powershell provider + var psProviders = ImportPowerShellProvider(request, instance.ProviderPath, instance.Version, shouldRefreshCache: force); + foreach (var p in psProviders) { + yield return p; + } + + } else { + LoadProviderAssembly(request, instance.ProviderPath, shouldRefreshCache: force); + var foo = _packageProviders.Where(each => each.Key.IsWildcardMatch(providerName)); + foreach (var p in foo) { + yield return p.Value; + } + } + } + } + } + + + private IEnumerable ImportPowerShellProvider(IHostApi request, string modulePath, Version requiredVersion, bool shouldRefreshCache) + { + request.Debug(string.Format(CultureInfo.CurrentCulture, "Calling ImportPowerShellProvider. providerName = '{0}', requiredVersion='{1}'", + modulePath, requiredVersion)); + + var powerShellMetaProvider = GetMetaProviderObject(request); + if (powerShellMetaProvider == null) { + yield break; + } + + //providerName can be a file path or name. + var instances = powerShellMetaProvider.LoadAvailableProvider(request.As(), modulePath, requiredVersion, shouldRefreshCache).ReEnumerable(); + + if (!instances.Any()) { + //A provider is not found + request.Error(Constants.Messages.UnknownProvider, ErrorCategory.InvalidOperation.ToString(), + modulePath, string.Format(Resources.Messages.UnknownProvider, modulePath)); + yield break; + + } + + foreach (var instance in instances) { + //Register the provider + var provider = instance.As(); + if (provider != null) { + //initialize the actual powershell package provider + if (provider.Provider == null) { + continue; + } + provider.Provider.InitializeProvider(request.As()); + + AddToProviderCacheTable(provider.ProviderName, provider); + + //initialize the warpper package provider + provider.Initialize(request); + + // addOrSet locks the collection anyway. + _packageProviders.AddOrSet(provider.ProviderName, provider); + + yield return provider; + } + } + } + + private IMetaProvider GetMetaProviderObject(IHostApi request) + { + //retrieve the powershell metaprovider object + if (_metaProviders.ContainsKey("PowerShell")) { + var powerShellMetaProvider = _metaProviders["PowerShell"]; + if (powerShellMetaProvider != null) { + return powerShellMetaProvider; + } + } + + request.Verbose(string.Format(CultureInfo.CurrentCulture, Resources.Messages.FailedPowerShellMetaProvider)); + return null; + } + + private bool CompareProvider(PackageProvider p1, PackageProvider p2) { + if (p1 == null && p2 == null) { + return true; + } + + if (p1 == null || p2 == null) { + return false; + } + + if ((p1.Name != null) && (p1.Name.EqualsIgnoreCase(p2.Name)) && (p1.ProviderName != null && p1.ProviderName.EqualsIgnoreCase(p2.ProviderName) && p1.Version == p2.Version)) { + return true; + } + return false; + } + + internal void AddToProviderCacheTable(string name, PackageProvider provider) { + lock (_providerCacheTable) { + if (_providerCacheTable.ContainsKey(name)) { + var list = _providerCacheTable[name]; + + var index = list.FindIndex(each => CompareProvider(each, provider)); + if (index != -1) { + //overwrite the cache only if the provider is loaded but the existing one not loaded + if (!list[index].IsLoaded && provider.IsLoaded) { + list[index] = provider; + } + } else { + _providerCacheTable[name].Add(provider); + } + } else { + var entry = new List { + provider + }; + _providerCacheTable.Add(name, entry); + } + } + } + + private void ResetProviderCachetable() { + + foreach (var list in _providerCacheTable.Values.WhereNotNull()) { + list.Clear(); + } + _providerCacheTable.Clear(); + + _packageProviders.ParallelForEach(each => AddToProviderCacheTable(each.Key, each.Value)); + } + + //Scan through the well-known providerAssemblies folder to find the providers that met the condition. + internal IEnumerable ScanAllProvidersFromProviderAssembliesLocation( + IHostApi request, + string providerName, + Version requiredVersion, + Version minimumVersion, + Version maximumVersion, + ProviderOption providerOption = ProviderOption.LatestVersion) { + + //if provider is installed in providername\version format + var providerFolder = ProviderAssembliesLocation.Distinct(new PathEqualityComparer(PathCompareOption.Full)).SelectMany(Directory.EnumerateDirectories); + + foreach (var providerNameFolder in providerFolder) { + + var name = Path.GetFileName(providerNameFolder); + + //check the providername folder + if (!string.IsNullOrWhiteSpace(providerName)) { + + if (string.IsNullOrWhiteSpace(providerNameFolder)) { + continue; + } + + if (string.IsNullOrWhiteSpace(name) || !name.IsWildcardMatch(providerName)) { + continue; + } + } + + var selectedVersions = Directory.EnumerateDirectories(providerNameFolder).Select(versionFolder => { + //check if the version is in a valid format. Ver will be 0 if TryParse fails and it won't be selected + Version ver; + if (System.Version.TryParse(Path.GetFileName(versionFolder), out ver)) { + return new { + folder = versionFolder, + version = (FourPartVersion)ver + }; + } + return null; + }).Where(each => each != null && each.version > 0L); + + selectedVersions = selectedVersions.Where(eachVersion => { + if ((requiredVersion == null) || eachVersion.version == (FourPartVersion)requiredVersion) { + if ((minimumVersion == null) || eachVersion.version >= (FourPartVersion)minimumVersion) { + if ((maximumVersion == null) || eachVersion.version <= (FourPartVersion)maximumVersion) { + return true; + } + } + } + return false; + }); + + //Get the version folders + var versionFolders = (providerOption == ProviderOption.AllProvider) ? + selectedVersions.Select(each => each.folder).Where(Directory.Exists) : + new[] {selectedVersions.OrderByDescending(each => each.version).Select(each => each.folder).FirstOrDefault(Directory.Exists)}; + + foreach (var assemblyFolder in versionFolders.WhereNotNull()) { + //we reached the provider assembly file path now + + var files = Directory.EnumerateFiles(assemblyFolder) + .Where(file => (file != null) && (Path.GetExtension(file).EqualsIgnoreCase(".dll") || Path.GetExtension(file).EqualsIgnoreCase(".exe")) + // we only check for dll that has manifest attached to it. (In case there are supporting assemblies in this folder) + && Manifest.LoadFrom(file).Any(manifest => Swidtag.IsSwidtag(manifest) && new Swidtag(manifest).IsApplicable(new Hashtable()))) + .ToArray(); + + //if found more than one dll with manifest is installed under a version folder, this is not allowed. warning here as enumerating for providers should continue + if (files.Any() && files.Count() > 1) { + request.Warning(string.Format(CultureInfo.CurrentCulture, Resources.Messages.SingleAssemblyAllowed, files.JoinWithComma())); + continue; + } + + // find modules that have the provider manifests + var filelist = files.Where(each => Manifest.LoadFrom(each).Any(manifest => Swidtag.IsSwidtag(manifest) && new Swidtag(manifest).IsApplicable(new Hashtable()))); + + if (!filelist.Any()) { + continue; + } + var version = Path.GetFileName(assemblyFolder); + var defaultPkgProvider = new DefaultPackageProvider(name, version); + var providerPath = files.FirstOrDefault(); + + var pkgProvider = new PackageProvider(defaultPkgProvider) + { + ProviderPath = providerPath, + Version = version, + IsLoaded = false + }; + + pkgProvider.SetSwidTag(providerPath); + + AddToProviderCacheTable(name, pkgProvider); + yield return providerPath; + } + } + + //check if assembly is installed at the top leverl folder. + var providerFiles = ProviderAssembliesLocation.Distinct(new PathEqualityComparer(PathCompareOption.Full)).SelectMany(Directory.EnumerateFiles) + .Where(each => each.FileExists() && (Path.GetExtension(each).EqualsIgnoreCase(".dll") || Path.GetExtension(each).EqualsIgnoreCase(".exe"))).ReEnumerable(); + + // found the assemblies at the top level folder. + // if a user is looking for a specific version & provider name, we are not be able to know the provider name or version without loading it. + // Thus, for the top level providers, we just need to load them for the backward compatibility. + if (providerFiles.Any()) { + request.Verbose(string.Format(CultureInfo.CurrentCulture, Resources.Messages.ProviderNameAndVersionNotAvailableFromFilePath, providerFiles.JoinWithComma())); + foreach (var provider in providerFiles) { + //the provider is installed at the top level. + + // find modules that have the provider manifests + if (Manifest.LoadFrom(provider).Any(manifest => Swidtag.IsSwidtag(manifest) && new Swidtag(manifest).IsApplicable(new Hashtable()))) { + + yield return provider; + } + } + } + } + + //Return all providers under the providerAssemblies folder + internal IEnumerable AllProvidersFromProviderAssembliesLocation(IHostApi request) { + try { + + return ScanAllProvidersFromProviderAssembliesLocation(request, null, null, null, null, ProviderOption.AllProvider).WhereNotNull().ToArray(); + } catch (Exception ex) { + request.Debug(ex.Message); + } + return Enumerable.Empty(); + } + + + //return the providers with latest version under the providerAssemblies folder + //This method only gets called during the initialization, i.e. LoadProviders(). + private IEnumerable ProvidersWithLatestVersionFromProviderAssembliesLocation(IHostApi request) { + try { + var providerPaths = ScanAllProvidersFromProviderAssembliesLocation(request, null, null, null, null, ProviderOption.LatestVersion).WhereNotNull().ToArray(); + + var notRootAssemblies = providerPaths.Where(each => !ProviderAssembliesLocation.ContainsIgnoreCase(Path.GetDirectoryName(each))).ToArray(); + var rootAssemblies = providerPaths.Where(each => ProviderAssembliesLocation.ContainsIgnoreCase(Path.GetDirectoryName(each))).ToArray(); + + var equalityComparer = new PathEqualityComparer(PathCompareOption.File); + //return the assemblies that are installed not directly under ProviderAssemblies root folder. + //For the assemblies under the root directory, we need to check further if the provider that has the later version + //installed under providername\version folder + //Convention: the providers are installed under providerassemblies\providername\version folder has the later version + //than those at the top root folder. + var assembliesUnderRootFolder = rootAssemblies.Where(rootPath => !notRootAssemblies.Any(element => equalityComparer.Equals(element, rootPath))); + + //for these assemblies not under the providerassemblies root folders but they have the same provider names, we return the latest version + var assembliesUnderVersionFolder = notRootAssemblies.GroupBy(Path.GetFileName).Select( + each => each.OrderByDescending(file => { + var versionFolder = Path.GetDirectoryName(file); + Version ver; + return !System.Version.TryParse(Path.GetFileName(versionFolder), out ver) ? new Version("0.0") : ver; + }).FirstOrDefault()).WhereNotNull(); + + //filter out the old nuget-anycpu if exists + assembliesUnderRootFolder = assembliesUnderRootFolder.Where(rootPath => !assembliesUnderVersionFolder.Any(element => new PathEqualityComparer(PathCompareOption.Nuget).Equals(element, rootPath))); + + return assembliesUnderVersionFolder.Concat(assembliesUnderRootFolder); + + } + catch (Exception ex) + { + request.Debug(ex.Message); + } + return Enumerable.Empty(); + } + + /// + /// This initializes the provider registry with the list of package providers. + /// (currently a hardcoded list, soon, registry driven) + /// + /// + internal void LoadProviders(IHostApi request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + //looks like registry needs to be here for supporting .msi packages + var providerAssemblies = (_initialized ? Enumerable.Empty() : _defaultProviders) + .Concat(GetProvidersFromRegistry(Registry.LocalMachine, "SOFTWARE\\MICROSOFT\\PACKAGEMANAGEMENT")) + .Concat(GetProvidersFromRegistry(Registry.CurrentUser, "SOFTWARE\\MICROSOFT\\PACKAGEMANAGEMENT")); + + providerAssemblies = providerAssemblies.Concat(ProvidersWithLatestVersionFromProviderAssembliesLocation(request)); + +#if DEEP_DEBUG + providerAssemblies = providerAssemblies.ToArray(); + + foreach (var each in providerAssemblies) { + request.Debug("possible assembly: {0}".format(each)); + } +#endif + + // find modules that have manifests + // todo: expand this out to validate the assembly is ok for this instance of PackageManagement. + providerAssemblies = providerAssemblies.Where(each => Manifest.LoadFrom(each).Any(manifest => Swidtag.IsSwidtag(manifest) && new Swidtag(manifest).IsApplicable(new Hashtable()))); + + // add inbox assemblies (don't require manifests, because they are versioned with the core) + +#if !COMMUNITY_BUILD + // todo: these should just be strong-named references. for now, just load them from the same directory. + providerAssemblies = providerAssemblies.Concat(new[] { + Path.Combine(BaseDir, "Microsoft.PackageManagement.MetaProvider.PowerShell.dll"), + Path.Combine(BaseDir, "Microsoft.PackageManagement.ArchiverProviders.dll"), + Path.Combine(BaseDir, "Microsoft.PackageManagement.CoreProviders.dll"), +#if !CORECLR + Path.Combine(BaseDir, "Microsoft.PackageManagement.MsuProvider.dll"), + Path.Combine(BaseDir, "Microsoft.PackageManagement.MsiProvider.dll") +#endif + }); +#endif + +#if DEEP_DEBUG + providerAssemblies = providerAssemblies.ToArray(); + + foreach (var each in providerAssemblies) { + request.Debug("possible assembly with manifest: {0}".format(each)); + } +#endif + + providerAssemblies = providerAssemblies.OrderByDescending(each => { + try { + // try to get a version from the file first + return (ulong)(FourPartVersion)FileVersionInfo.GetVersionInfo(each); + } catch { + // otherwise we can't make a distinction. + return (ulong)0; + } + }); + + providerAssemblies = providerAssemblies.Distinct(new PathEqualityComparer(PathCompareOption.FileWithoutExtension)); + + // there is no trouble with loading providers concurrently. +#if DEEP_DEBUG + providerAssemblies.SerialForEach(providerAssemblyName => { +#else + providerAssemblies.ParallelForEach(providerAssemblyName => { +#endif + LoadProviderAssembly(request, providerAssemblyName, false); + }); +#if DEEP_DEBUG + WaitForDebugger(); +#endif + } + +#if DEEP_DEBUG + internal void WaitForDebugger() { + if (!System.Diagnostics.Debugger.IsAttached) { + Console.Beep(500, 2000); + while (!System.Diagnostics.Debugger.IsAttached) { + System.Threading.Thread.Sleep(1000); + Console.Beep(500, 200); + } + } + } +#endif + /// + /// Dynamic providers are the ones that are not installed with the core itself. + /// + internal IEnumerable DynamicProviders { + get { + return _packageProviders.Values.Where(each => !each.ProviderPath.StartsWith(BaseDir, StringComparison.OrdinalIgnoreCase)); + } + } + + private static IEnumerator GetProvidersFromRegistry(RegistryKey registryKey, string p) { + RegistryKey key; + try { +#if !CORECLR + key = registryKey.OpenSubKey(p, RegistryKeyPermissionCheck.ReadSubTree, RegistryRights.ReadKey); +#else + key = registryKey.OpenSubKey(p); +#endif + } catch { + yield break; + } + + if (key == null) { + yield break; + } + + foreach (var name in key.GetValueNames()) { + yield return key.GetValue(name).ToString(); + } + } + + public IEnumerable GetAllSourceNames(IHostApi request) { + return _packageProviders.Values.SelectMany(each => each.ResolvePackageSources(request)); + } + + + /// + /// Searches for the assembly, interrogates it for it's providers and then proceeds to load + /// + /// + /// + /// + /// + internal bool LoadProviderAssembly(IHostApi request, string providerAssemblyName, bool shouldRefreshCache) { + request.Debug(request.FormatMessageString("Trying provider assembly: {0}", providerAssemblyName)); + var assemblyPath = FindAssembly(providerAssemblyName); + + if (assemblyPath != null) { + + try { + byte[] hash = null; + using (var stream = File.Open(assemblyPath, FileMode.Open, FileAccess.Read, FileShare.Read)) { + hash = System.Security.Cryptography.MD5.Create().ComputeHash(stream); + } + lock (_providerFiles) { + if (_providerFiles.ContainsKey(assemblyPath)) { + // have we tried this file before? + //if the exactly same provider is loaded already, skip the processed assembly. + + if (_providerFiles[assemblyPath].SequenceEqual(hash) && !shouldRefreshCache) { + // and it's the exact same file? + request.Verbose(string.Format(CultureInfo.CurrentCulture, Resources.Messages.SkippingProcessedAssembly, assemblyPath)); + return false; + } + // it's a different file in the same path? -force exists? + // we're gonna let it try the new file. + _providerFiles.Remove(assemblyPath); + } + + request.Debug(request.FormatMessageString("Attempting loading of assembly: {0}", assemblyPath)); + // record that this file is being loaded. + _providerFiles.Add(assemblyPath, hash); + } + if (AcquireProviders(assemblyPath, request, shouldRefreshCache)) { + request.Debug(request.FormatMessageString("SUCCESS provider assembly: {0}", providerAssemblyName)); + return true; + } + } catch (Exception e) { + e.Dump(); + + lock (_providerFiles) { + // can't create hash from file? + // we're not going to try and load this. + // all we can do is record the name. + if (!_providerFiles.ContainsKey(assemblyPath)) { + _providerFiles.Add(assemblyPath, new byte[0]); + } + } + } + } + request.Debug(request.FormatMessageString("FAILED provider assembly: {0}", providerAssemblyName)); + return false; + } + + /// + /// PROTOTYPE -- extremely simplified assembly locator. + /// + /// + /// + private string FindAssembly(string assemblyName) { + try { + string fullPath; + // is the name given a strong name? + if (assemblyName.Contains(',')) { + // looks like a strong name + // todo: not there yet... + return null; + } + + // is it a path? + if (assemblyName.Contains('\\') || assemblyName.Contains('/') || assemblyName.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || assemblyName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) { + fullPath = Path.GetFullPath(assemblyName); + if (File.Exists(fullPath)) { + return fullPath; + } + if (File.Exists(fullPath + ".dll")) { + return fullPath; + } + + // lets see if the assembly name is in the same directory as the current assembly... + try { + fullPath = Path.Combine(Path.GetDirectoryName(CurrentAssemblyLocation), assemblyName); + if (File.Exists(fullPath)) { + return fullPath; + } + if (File.Exists(fullPath + ".dll")) { + return fullPath; + } + } catch { + } + } + // must be just just a plain name. + + // todo: search the GAC too? + + // search the local folder. + fullPath = Path.GetFullPath(assemblyName + ".dll"); + if (File.Exists(fullPath)) { + return fullPath; + } + + // try next to where we are. + fullPath = Path.Combine(Path.GetDirectoryName(CurrentAssemblyLocation), assemblyName + ".dll"); + if (File.Exists(fullPath)) { + return fullPath; + } + } catch (Exception e) { + e.Dump(); + } + return null; + } + + [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.Reflection.Assembly.LoadFrom", Justification = "This is a plugin loader. It *needs* to do that.")] + internal bool AcquireProviders(string assemblyPath, IHostApi request, bool shouldRefreshCache) { + var found = false; + try { + + Assembly assembly = null; + +#if !CORECLR + assembly = Assembly.LoadFrom(assemblyPath); +#else + assembly = Microsoft.PowerShell.CoreCLR.AssemblyExtensions.LoadFrom(assemblyPath); +#endif + if (assembly == null) { + return false; + } + + var asmVersion = GetAssemblyVersion(assembly); + request.Debug("Acquiring providers for assembly" + assemblyPath); + + assembly.FindCompatibleTypes().AsyncForEach(metaProviderClass => { + request.Debug("Registering providers via metaproviders for assembly " + metaProviderClass); + found |= RegisterProvidersViaMetaProvider(metaProviderClass.Create(), asmVersion, request); + }) + .Concat(assembly.FindCompatibleTypes().AsyncForEach(packageProviderClass => { + + try { + //Handling C# based providers + var packageProvider = RegisterPackageProvider(packageProviderClass.Create(), asmVersion, request, shouldRefreshCache); + if (packageProvider != null) { + found = true; + packageProvider.ProviderPath = assemblyPath; + packageProvider.SetSwidTag(assemblyPath); + } + + } catch (Exception ex) { + request.Debug(ex.Message); + } + })) + + .Concat(assembly.FindCompatibleTypes().AsyncForEach(serviceProviderClass => { + var archiver = RegisterArchiver(serviceProviderClass.Create(), asmVersion, request); + if (archiver != null) { + found = true; + archiver.ProviderPath = assemblyPath; + archiver.SetSwidTag(assemblyPath); + } + + })) + .Concat(assembly.FindCompatibleTypes().AsyncForEach(serviceProviderClass => { + var downloader = RegisterDownloader(serviceProviderClass.Create(), asmVersion, request); + if (downloader != null) { + found = true; + downloader.ProviderPath = assemblyPath; + downloader.SetSwidTag(assemblyPath); + } + + })).WaitAll(); + + } catch (Exception e) { + request.Debug(e.Message); + request.Debug(e.StackTrace); + } + return found; + } + + private static FourPartVersion GetAssemblyVersion(Assembly asm) { + FourPartVersion result = 0; + + result = asm.GetName().Version; + + if (result == 0) { + // what? No assembly version? + // fallback to the file version of the assembly +#if !CORECLR + var assemblyLocation = asm.Location; +#else + var assemblyLocation = asm.ManifestModule.FullyQualifiedName; +#endif + if (!string.IsNullOrWhiteSpace(assemblyLocation) && File.Exists(assemblyLocation)) { + result = FileVersionInfo.GetVersionInfo(assemblyLocation); + if (result == 0) { + // no file version either? + // use the date I guess. + try { + result = new FileInfo(assemblyLocation).LastWriteTime; + } catch { + } + } + } + + if (result == 0) { + // still no version? + // I give up. call it 0.0.0.1 + result = "0.0.0.1"; + } + } + return result; + } + + /// + /// Register the package provider + /// + /// Package provider object + /// assembly version info + /// + /// should refresh the internal provider list + /// + private PackageProvider RegisterPackageProvider(IPackageProvider provider, + FourPartVersion asmVersion, + IHostApi request, + bool shouldRefreshCache) { + + string name = null; + try { + if (provider == null) { + return null; + } + FourPartVersion ver = provider.GetProviderVersion(); + var version = ver == 0 ? asmVersion : ver; + name = provider.GetPackageProviderName(); + if (string.IsNullOrWhiteSpace(name)) { + return null; + } + + // Initialize the provider before locking the collection + // that way we're not blocking others on non-deterministic actions. + request.Debug("Initializing provider '{0}'".format(name)); + provider.InitializeProvider(request.As()); + request.Debug("Provider '{0}' Initialized".format(name)); + + lock (_packageProviders) { + //Check if the provider is loaded already. + if (_packageProviders.ContainsKey(name)) { + //if no -force, do nothing + if (!shouldRefreshCache) { + request.Verbose(string.Format(CultureInfo.CurrentCulture, Resources.Messages.SkipPreviousProcessedProvider, name)); + + //add the provider to the list + var pkgprovider = new PackageProvider(provider) { + Version = version, + IsLoaded = true + }; + AddToProviderCacheTable(name, pkgprovider); + return pkgprovider; + } else { + //looks like -force is used, we need to remove the old provider first. + // this won't remove the plugin domain and unload the code yet + _packageProviders.Remove(name); + } + } + } + + request.Debug("Using Package Provider {0}".format(name)); + var packageProvider = new PackageProvider(provider) { + Version = version + }; + + //initialize the package provider + packageProvider.Initialize(request); + + // addOrSet locks the collection anyway. + _packageProviders.AddOrSet(name, packageProvider); + packageProvider.IsLoaded = true; + request.Debug("The provider '{0}' is imported".format(name)); + + //add the provider to the list + AddToProviderCacheTable(name, packageProvider); + return packageProvider; + } catch (Exception e) { + request.Debug("Provider '{0}' Failed to import".format(name)); + e.Dump(); + } + return null; + } + + private Archiver RegisterArchiver(IArchiver provider, FourPartVersion asmVersion, IHostApi request) { + string name = null; + try { + FourPartVersion ver = provider.GetProviderVersion(); + var version = ver == 0 ? asmVersion : ver; + name = provider.GetArchiverName(); + if (string.IsNullOrWhiteSpace(name)) { + return null; + } + + // Initialize the provider before locking the collection + // that way we're not blocking others on non-deterministic actions. + request.Debug("Initializing provider '{0}'".format(name)); + provider.InitializeProvider(request.As()); + request.Debug("Provider '{0}' Initialized".format(name)); + + lock (Archivers) { + if (Archivers.ContainsKey(name)) { + if (version > Archivers[name].Version) { + // remove the old provider first. + // todo: this won't remove the plugin domain and unload the code yet + // we'll have to do that later. + + Archivers.Remove(name); + } + else { + return null; + } + } + request.Debug("Using Archiver Provider {0}".format(name)); + var archiver = new Archiver(provider) { + Version = version, + IsLoaded = true + }; + + archiver.Initialize(request); + Archivers.AddOrSet(name, archiver); + return archiver; + } + } + catch (Exception e) { + request.Debug("Provider '{0}' Failed".format(name)); + e.Dump(); + } + return null; + } + + private Downloader RegisterDownloader(IDownloader provider, FourPartVersion asmVersion, IHostApi request) { + string name = null; + try { + FourPartVersion ver = provider.GetProviderVersion(); + var version = ver == 0 ? asmVersion : ver; + name = provider.GetDownloaderName(); + if (string.IsNullOrWhiteSpace(name)) { + return null; + } + + // Initialize the provider before locking the collection + // that way we're not blocking others on non-deterministic actions. + request.Debug("Initializing provider '{0}'".format(name)); + provider.InitializeProvider(request.As()); + request.Debug("Provider '{0}' Initialized".format(name)); + + lock (Downloaders) { + if (Downloaders.ContainsKey(name)) { + if (version > Downloaders[name].Version) { + // remove the old provider first. + // todo: this won't remove the plugin domain and unload the code yet + // we'll have to do that later. + + Downloaders.Remove(name); + } else { + return null; + } + } + request.Debug("Using Downloader Provider {0}".format(name)); + + var downloader = new Downloader(provider) { + Version = version, + IsLoaded = true + }; + + downloader.Initialize(request); + Downloaders.AddOrSet(name, downloader); + return downloader; + } + } catch (Exception e) { + request.Debug("Provider '{0}' Failed".format(name)); + e.Dump(); + } + return null; + } + + internal bool TryLoadProviderViaMetaProvider(string metaproviderName, string providerNameOrPath, IHostApi request ) { + if (_metaProviders.ContainsKey(metaproviderName)) { + var metaProvider = _metaProviders[metaproviderName]; + + request.Debug("Using MetaProvider '{0}' to attempt to load provider from '{1}'".format(metaproviderName, providerNameOrPath)); + + return LoadViaMetaProvider( _metaProviders[metaproviderName], providerNameOrPath, metaProvider.GetProviderVersion(),request); + } + request.Debug("MetaProvider '{0}' is not recognized".format(metaproviderName)); + return false; + } + + internal bool RegisterProvidersViaMetaProvider(IMetaProvider provider, FourPartVersion asmVersion, IHostApi request) { + request.Debug("Trying to register metaprovider"); + var found = false; + var metaProviderName = provider.GetMetaProviderName(); + + lock (_metaProviders) { + if (!_metaProviders.ContainsKey(metaProviderName)) { + // Meta Providers can't be replaced at this point + _metaProviders.AddOrSet(metaProviderName, provider); + } + } + + try { + provider.InitializeProvider(request.As()); + provider.GetProviderNames().ParallelForEach(name => { + found = LoadViaMetaProvider(provider, name, asmVersion, request); + }); + } catch (Exception e) { + e.Dump(); + } + return found; + } + + private bool LoadViaMetaProvider(IMetaProvider metaProvider, string name, FourPartVersion asmVersion, IHostApi request) { + var found = false; + + var instance = metaProvider.CreateProvider(name); + if (instance != null) { + // check if it's a Package Provider + if (typeof (IPackageProvider).CanDynamicCastFrom(instance)) { + try { + var packageProvider = RegisterPackageProvider(instance.As(), asmVersion, request, false); + if (packageProvider != null) { + found = true; + packageProvider.IsLoaded = true; + packageProvider.ProviderPath = metaProvider.GetProviderPath(name); + packageProvider.SetSwidTag(packageProvider.ProviderPath); + } + + } catch (Exception e) { + e.Dump(); + } + } + + // check if it's a Services Provider + if (typeof (IArchiver).CanDynamicCastFrom(instance)) { + try { + var archiver = RegisterArchiver(instance.As(), asmVersion, request); + if (archiver != null) { + found = true; + archiver.IsLoaded = true; + archiver.ProviderPath = metaProvider.GetProviderPath(name); + archiver.SetSwidTag(archiver.ProviderPath); + } + + } catch (Exception e) { + e.Dump(); + } + } + + if (typeof (IDownloader).CanDynamicCastFrom(instance)) { + try { + var downloader = RegisterDownloader(instance.As(), asmVersion, request); + if (downloader != null) { + found = true; + downloader.ProviderPath = metaProvider.GetProviderPath(name); + downloader.SetSwidTag(downloader.ProviderPath); + downloader.IsLoaded = true; + } + + } catch (Exception e) { + e.Dump(); + } + } + } + return found; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Implementation/PackageProvider.cs b/src/Microsoft.PackageManagement/Implementation/PackageProvider.cs new file mode 100644 index 000000000..7034fc944 --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/PackageProvider.cs @@ -0,0 +1,216 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Implementation { + using System; + using Internal; + using Internal.Api; + using Internal.Implementation; + using Internal.Providers; + using Internal.Utility.Async; + using Internal.Utility.Plugin; + using Packaging; + + public class PackageProvider : ProviderBase { + private string _name; + + internal PackageProvider(IPackageProvider provider) : base(provider) { + } + + public override string Name { + get { + if (string.IsNullOrWhiteSpace(base.Name)) + { + base.Name = ProviderName; + } + + return ProviderName; + } + } + + public override string ProviderName { + get { + return _name ?? (_name = Provider.GetPackageProviderName()); + } + } + + // Friendly APIs + + public IAsyncEnumerable AddPackageSource(string name, string location, bool trusted, IHostApi requestObject) { + return new PackageSourceRequestObject(this, requestObject ?? new object().As(), request => Provider.AddPackageSource(name, location, trusted, request)); + } + + public IAsyncEnumerable RemovePackageSource(string name, IHostApi requestObject) { + return new PackageSourceRequestObject(this, requestObject ?? new object().As(), request => Provider.RemovePackageSource(name, request)); + } + + public IAsyncEnumerable FindPackageByUri(Uri uri, IHostApi requestObject) { + if (!IsSupportedScheme(uri)) { + return new EmptyAsyncEnumerable(); + } + + return new SoftwareIdentityRequestObject(this, requestObject ?? new object().As(), request => Provider.FindPackageByUri(uri, 0, request), Constants.PackageStatus.Available); + } + + public IAsyncEnumerable FindPackageByFile(string filename, IHostApi requestObject) { + if (!IsSupportedFile(filename)) { + return new EmptyAsyncEnumerable(); + } + return new SoftwareIdentityRequestObject(this, requestObject ?? new object().As(), request => Provider.FindPackageByFile(filename, 0, request), Constants.PackageStatus.Available); + } + + public IAsyncEnumerable FindPackages(string[] names, string requiredVersion, string minimumVersion, string maximumVersion, IHostApi requestObject) { + if (requestObject == null) { + throw new ArgumentNullException("requestObject"); + } + + if (names == null) { + throw new ArgumentNullException("names"); + } + + if (names.Length == 0) { + return FindPackage(null, requiredVersion, minimumVersion, maximumVersion, requestObject); + } + + if (names.Length == 1) { + return FindPackage(names[0], requiredVersion, minimumVersion, maximumVersion, requestObject); + } + + return new SoftwareIdentityRequestObject(this, requestObject, request => { + foreach (var name in names) { + Provider.FindPackage(name, requiredVersion, minimumVersion, maximumVersion, 0, request); + } + }, Constants.PackageStatus.Available); + } + + public IAsyncEnumerable FindPackagesByUris(Uri[] uris, IHostApi requestObject) { + if (requestObject == null) { + throw new ArgumentNullException("requestObject"); + } + + if (uris == null) { + throw new ArgumentNullException("uris"); + } + + if (uris.Length == 0) { + return new EmptyAsyncEnumerable(); + } + + if (uris.Length == 1) { + return FindPackageByUri(uris[0],requestObject); + } + + return new SoftwareIdentityRequestObject(this, requestObject, request => { + foreach (var uri in uris) { + Provider.FindPackageByUri(uri, 0, request); + } + }, Constants.PackageStatus.Available); + } + + public IAsyncEnumerable FindPackagesByFiles(string[] filenames, IHostApi requestObject) { + if (requestObject == null) { + throw new ArgumentNullException("requestObject"); + } + + if (filenames == null) { + throw new ArgumentNullException("filenames"); + } + + if (filenames.Length == 0) { + return new EmptyAsyncEnumerable(); + } + + if (filenames.Length == 1) { + return FindPackageByFile(filenames[0], requestObject); + } + + return new SoftwareIdentityRequestObject(this, requestObject, request => { + foreach (var file in filenames) { + Provider.FindPackageByFile(file, 0, request); + } + }, Constants.PackageStatus.Available); + } + + public IAsyncEnumerable FindPackage(string name, string requiredVersion, string minimumVersion, string maximumVersion, IHostApi requestObject) { + if (requestObject != null) + { + requestObject.Debug(String.Format(System.Globalization.CultureInfo.CurrentCulture, "PackageProvider::FindPackage with name {0}", name)); + } + return new SoftwareIdentityRequestObject(this, requestObject ?? new object().As(), request => Provider.FindPackage(name, requiredVersion, minimumVersion, maximumVersion,0, request), Constants.PackageStatus.Available); + } + + public IAsyncEnumerable GetInstalledPackages(string name, string requiredVersion, string minimumVersion, string maximumVersion, IHostApi requestObject) { + return new SoftwareIdentityRequestObject(this, requestObject ?? new object().As(), request => Provider.GetInstalledPackages(name, requiredVersion, minimumVersion, maximumVersion, request), Constants.PackageStatus.Installed); + } + + public SoftwareIdentity GetPackageDetails(SoftwareIdentity softwareIdentity, IHostApi requestObject) { + if (requestObject == null) { + throw new ArgumentNullException("requestObject"); + } + + if (softwareIdentity == null) { + throw new ArgumentNullException("softwareIdentity"); + } + + new PackageDetailsRequestObject(this, requestObject, softwareIdentity, request => Provider.GetPackageDetails(softwareIdentity.FastPackageReference, request)).Wait(); + + return softwareIdentity; + } + + public IAsyncEnumerable InstallPackage(SoftwareIdentity softwareIdentity, IHostApi requestObject) { + if (requestObject == null) { + throw new ArgumentNullException("requestObject"); + } + + if (softwareIdentity == null) { + throw new ArgumentNullException("softwareIdentity"); + } + + // if the provider didn't say this was trusted, we should ask the user if it's ok. + if (!softwareIdentity.FromTrustedSource) { + try { + if (!requestObject.ShouldContinueWithUntrustedPackageSource(softwareIdentity.Name, softwareIdentity.Source)) { + requestObject.Warning(requestObject.FormatMessageString(Constants.Messages.UserDeclinedUntrustedPackageInstall, softwareIdentity.Name)); + return new EmptyAsyncEnumerable(); + } + } catch { + return new EmptyAsyncEnumerable(); + } + } + + return new SoftwareIdentityRequestObject(this, requestObject ?? new object().As(), request => Provider.InstallPackage(softwareIdentity.FastPackageReference, request), Constants.PackageStatus.Installed); + } + + public IAsyncEnumerable UninstallPackage(SoftwareIdentity softwareIdentity, IHostApi requestObject) { + return new SoftwareIdentityRequestObject(this, requestObject ?? new object().As(), request => Provider.UninstallPackage(softwareIdentity.FastPackageReference, request), Constants.PackageStatus.Uninstalled); + } + + public IAsyncEnumerable ResolvePackageSources(IHostApi requestObject) { + return new PackageSourceRequestObject(this, requestObject ?? new object().As(), request => Provider.ResolvePackageSources(request)); + } + + public IAsyncEnumerable DownloadPackage(SoftwareIdentity softwareIdentity, string destinationFilename, IHostApi requestObject) + { + if (requestObject == null) { + throw new ArgumentNullException("requestObject"); + } + + if (softwareIdentity == null) { + throw new ArgumentNullException("softwareIdentity"); + } + + return new SoftwareIdentityRequestObject(this, requestObject, request => Provider.DownloadPackage(softwareIdentity.FastPackageReference, destinationFilename, request), Constants.PackageStatus.Downloaded); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Implementation/PackageSourceRequestObject.cs b/src/Microsoft.PackageManagement/Implementation/PackageSourceRequestObject.cs new file mode 100644 index 000000000..13472d9d6 --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/PackageSourceRequestObject.cs @@ -0,0 +1,72 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Implementation { + using System; + using Api; + using PackageManagement.Implementation; + using PackageManagement.Packaging; + using Utility.Extensions; + using System.Globalization; + + internal class PackageSourceRequestObject : EnumerableRequestObject { + private PackageSource _currentItem; + + public PackageSourceRequestObject(ProviderBase provider, IHostApi request, Action action) + : base(provider, request, action) { + InvokeImpl(); + } + + private void CommitPackageSource() { + if (_currentItem != null) { + Results.Add(_currentItem); + } + _currentItem = null; + } + + public override bool YieldPackageSource(string name, string location, bool isTrusted, bool isRegistered, bool isValidated) { + Debug(String.Format(CultureInfo.CurrentCulture, "Yielding packge source for {0} at location {1}", name, location)); + Activity(); + + CommitPackageSource(); + + _currentItem = new PackageSource { + Name = name, + Location = location, + Provider = Provider as PackageProvider, + IsTrusted = isTrusted, + IsRegistered = isRegistered, + IsValidated = isValidated, + }; + return !IsCanceled; + } + + public override bool YieldKeyValuePair(string key, string value) { + Activity(); + + if (_currentItem == null) { + Console.WriteLine("TODO: SHOULD NOT GET HERE [(PackageSource)YieldKeyValuePair] ================================================"); + return !IsCanceled; + } + + _currentItem.DetailsCollection.AddOrSet(key, value); + return !IsCanceled; + } + + protected override void Complete() { + CommitPackageSource(); + base.Complete(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Implementation/ProviderBase.cs b/src/Microsoft.PackageManagement/Implementation/ProviderBase.cs new file mode 100644 index 000000000..188b743ea --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/ProviderBase.cs @@ -0,0 +1,210 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Implementation { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Linq; + using System.Xml.Linq; + using System.Text.RegularExpressions; + using Api; + using Packaging; + using Providers; + using Utility.Async; + using Utility.Extensions; + using Utility.Plugin; + using Utility.Versions; + using File = System.IO.File; + using Microsoft.PackageManagement.Internal.Utility.Platform; + using PackageManagement.Packaging; + + public abstract class ProviderBase : SoftwareIdentity + { + public abstract new string ProviderName {get;} + } + + public abstract class ProviderBase : ProviderBase where T : IProvider { + private List _dynamicOptions; + private Dictionary> _features; + private bool _initialized; + private byte[][] _magicSignatures; + private string[] _supportedFileExtensions; + private string[] _supportedSchemes; + private FourPartVersion _version; + + protected ProviderBase(T provider) { + Provider = provider; + } + + internal new T Provider { get; private set; } + + [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is required for the PowerShell Providers.")] + public IDictionary> Features { + get { + // todo: this dictionary should be read only (.net 4.0 doesn't have that!) + return _features; + } + } + + public new FourPartVersion Version { + get { + return _version; + } + set { + if (_version == 0) { + _version = value; + base.Version = value.ToString(); + } + } + } + + internal bool IsLoaded {get; set;} + + public string ProviderPath { get; set;} + + /// + /// Set swidtag based on provider file + /// + /// + public void SetSwidTag(string providerPath) + { + if (!string.IsNullOrWhiteSpace(providerPath)) + { + // check whether there is swidtag attached to the provider path + var swid = Manifest.LoadFrom(providerPath).FirstOrDefault(manifest => Swidtag.IsSwidtag(manifest)); + + if (swid != null) + { + // give the manifest to the providers to populate swidtag fields + SetSwidTag(new XDocument(new XDeclaration("1.0", "UTF-8", "yes"), swid)); + } + } + } + + public IEnumerable SupportedFileExtensions { + get { + return (_supportedFileExtensions ?? (_supportedFileExtensions = Features.ContainsKey(Constants.Features.SupportedExtensions) ? Features[Constants.Features.SupportedExtensions].ToArray() : Constants.Empty)); + } + } + + public IEnumerable SupportedUriSchemes { + get { + return (_supportedSchemes ?? (_supportedSchemes = Features.ContainsKey(Constants.Features.SupportedSchemes) ? Features[Constants.Features.SupportedSchemes].ToArray() : Constants.Empty)); + } + } + + internal byte[][] MagicSignatures { + get { + return _magicSignatures ?? (_magicSignatures = Features.ContainsKey(Constants.Features.MagicSignatures) ? Features[Constants.Features.MagicSignatures].Select(each => each.FromHex()).ToArray() : new byte[][] {}); + } + } + + [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "This is required for the PowerShell Providers.")] + public List DynamicOptions { + get { + if (_dynamicOptions == null) { + var nullHostApi = new object().As(); + var result = new List(); + result.AddRange(GetDynamicOptions(OptionCategory.Install, nullHostApi)); + result.AddRange(GetDynamicOptions(OptionCategory.Package, nullHostApi)); + result.AddRange(GetDynamicOptions(OptionCategory.Provider, nullHostApi)); + result.AddRange(GetDynamicOptions(OptionCategory.Source, nullHostApi)); + if ((Features != null) && Features.ContainsKey("IsChainingProvider")) { + // chaining package providers should not cache results + return result; + } + + _dynamicOptions = result; + } + return _dynamicOptions; + } + } + + public virtual bool IsSupportedFileName(string filename) { + try { + var extension = System.IO.Path.GetExtension(filename); + if (!string.IsNullOrWhiteSpace(extension)) { + return SupportedFileExtensions.ContainsIgnoreCase(extension); + } + } catch { + } + return false; + } + + public virtual bool IsSupportedFile(string filename) { + if (filename == null) { + throw new ArgumentNullException("filename"); + } + if (filename.FileExists()) { + var buffer = new byte[1024]; + var sz = 0; + try { + using (var file = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.Read)) { + sz = file.Read(buffer, 0, 1024); + } + return MagicSignatures.Any(magic => BufferMatchesMagicBytes(magic, buffer, sz)); + } catch { + // not openable. whatever. + } + } + return false; + } + + public virtual bool IsSupportedFile(byte[] header) { + return MagicSignatures.Any(magic => BufferMatchesMagicBytes(magic, header, header.Length)); + } + + public virtual bool IsSupportedScheme(Uri uri) { + try { + return SupportedUriSchemes.ContainsIgnoreCase(uri.Scheme); + } catch { + } + return false; + } + + private bool BufferMatchesMagicBytes(byte[] magic, byte[] buffer, int maxSize) { + if (magic.Length <= maxSize) { + return !magic.Where((t, i) => t != buffer[i]).Any(); + } + return false; + } + + public bool IsMethodImplemented(string methodName) { + return Provider.IsMethodImplemented(methodName); + } + + internal void Initialize(IHostApi request) { + if (!_initialized) { + _features = GetFeatures(request).Value; + _initialized = true; + } + } + + public IAsyncValue>> GetFeatures(IHostApi requestObject) { + if (requestObject == null) { + throw new ArgumentNullException("requestObject"); + } + + return new DictionaryRequestObject(this, requestObject, request => Provider.GetFeatures(request)); + } + + public IAsyncEnumerable GetDynamicOptions(OptionCategory category, IHostApi requestObject) { + requestObject = requestObject ?? new object().As(); + + return new DynamicOptionRequestObject(this, requestObject, request => Provider.GetDynamicOptions(category.ToString(), request), category); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Implementation/ProviderServicesImpl.cs b/src/Microsoft.PackageManagement/Implementation/ProviderServicesImpl.cs new file mode 100644 index 000000000..ae3b2e482 --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/ProviderServicesImpl.cs @@ -0,0 +1,236 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Implementation { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text.RegularExpressions; + using Api; + using PackageManagement.Packaging; + using Utility.Extensions; + using Utility.Platform; + using Utility.Plugin; + using Process = System.Diagnostics.Process; + + internal class ProviderServicesImpl : IProviderServices { + internal static IProviderServices Instance = new ProviderServicesImpl(); + private static readonly Regex _canonicalPackageRegex = new Regex(@"([^:]*?):([^/\#]*)/?([^#]*)\#?(.*)"); + + private PackageManagementService PackageManagementService { + get { + return PackageManager.Instance as PackageManagementService; + } + } + + public bool IsElevated { + get { + return AdminPrivilege.IsElevated; + } + } + + public IEnumerable FindPackageByCanonicalId(string canonicalId, IRequest requestObject) { + if (requestObject == null) { + throw new ArgumentNullException("requestObject"); + } + + return PackageManagementService.FindPackageByCanonicalId(canonicalId, requestObject); + } + + public string GetCanonicalPackageId(string providerName, string packageName, string version, string source) { + return SoftwareIdentity.CreateCanonicalId(providerName, packageName, version, source); + } + + public string ParseProviderName(string canonicalPackageId) { + return _canonicalPackageRegex.Match(canonicalPackageId).Groups[1].Value; + } + + public string ParsePackageName(string canonicalPackageId) { + return _canonicalPackageRegex.Match(canonicalPackageId).Groups[2].Value; + } + + public string ParsePackageVersion(string canonicalPackageId) { + return _canonicalPackageRegex.Match(canonicalPackageId).Groups[3].Value; + } + + public string ParsePackageSource(string canonicalPackageId) { + return _canonicalPackageRegex.Match(canonicalPackageId).Groups[4].Value; + } + + public bool IsSupportedArchive(string localFilename, IRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + if (!request.IsCanceled) { + return PackageManagementService.Archivers.Values.Any(archiver => archiver.IsSupportedFile(localFilename)); + } + return false; + } + + public string DownloadFile(Uri remoteLocation, string localFilename, IRequest request) { + return DownloadFile(remoteLocation, localFilename, -1, true, request); + } + + public string DownloadFile(Uri remoteLocation, string localFilename, int timeoutMilliseconds, bool showProgress, IRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + if (!request.IsCanceled) { + // check the Uri type, see if we have anyone who can handle that + // if so, call that provider's download file + if (remoteLocation == null) { + throw new ArgumentNullException("remoteLocation"); + } + + foreach (var downloader in PackageManagementService.Downloaders.Values) { + if (downloader.SupportedUriSchemes.Contains(remoteLocation.Scheme, StringComparer.OrdinalIgnoreCase)) { + return downloader.DownloadFile(remoteLocation, localFilename,timeoutMilliseconds, showProgress, request); + } + } + + Error(request, ErrorCategory.NotImplemented, remoteLocation.Scheme, Constants.Messages.ProtocolNotSupported, remoteLocation.Scheme); + } + return null; + } + + public IEnumerable UnpackArchive(string localFilename, string destinationFolder, IRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + if (!request.IsCanceled) { + // check who supports the archive type + // and call that provider. + if (request == null) { + throw new ArgumentNullException("request"); + } + + foreach (var archiver in PackageManagementService.Archivers.Values) { + if (archiver.IsSupportedFile(localFilename)) { + return archiver.UnpackArchive(localFilename, destinationFolder, request); + } + } + Error(request, ErrorCategory.NotImplemented, localFilename, Constants.Messages.UnsupportedArchive); + } + return Enumerable.Empty(); + } + + public bool Install(string fileName, string additionalArgs, IRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + if (!request.IsCanceled) { + if (String.IsNullOrWhiteSpace(fileName)) { + return false; + } + + // high-level api for simply installing a file + // returns false if unsuccessful. + foreach (var provider in PackageManager.Instance.PackageProviders) { + var packages = provider.FindPackageByFile(fileName, request).ToArray(); + if (packages.Length > 0) { + // found a provider that can handle this package. + // install with this provider + // ToDo: @FutureGarrett -- we need to be able to handle priorities and who wins... + foreach (var package in packages) { + foreach (var installedPackage in provider.InstallPackage(package, request.Extend(new { + GetOptionValues = new Func>(key => { + if (key.EqualsIgnoreCase("additionalArguments")) { + return new[] {additionalArgs}; + } + return request.GetOptionValues(key); + }) + }))) { + Debug(request, "Installed internal package {0}", installedPackage.Name); + } + } + return true; + } + } + } + return false; + } + + public bool IsSignedAndTrusted(string filename, IRequest request) { + if (request == null) { + throw new ArgumentNullException("request"); + } + + if (!request.IsCanceled) { + if (String.IsNullOrWhiteSpace(filename) || !filename.FileExists()) { + return false; + } + + Debug(request, "Calling 'ProviderService::IsSignedAndTrusted, '{0}'", filename); + + var wtd = new WinTrustData(filename); + var result = NativeMethods.WinVerifyTrust(new IntPtr(-1), new Guid("{00AAC56B-CD44-11d0-8CC2-00C04FC295EE}"), wtd); + return result == WinVerifyTrustResult.Success; + } + return false; + } + + public int StartProcess(string filename, string arguments, bool requiresElevation, out string standardOutput, IRequest requestObject) { + Process p = new Process(); + + if (requiresElevation) { + p.StartInfo.UseShellExecute = true; + } else { + p.StartInfo.UseShellExecute = false; + p.StartInfo.RedirectStandardOutput = true; + } + + p.StartInfo.CreateNoWindow = true; + p.StartInfo.FileName = filename; + + if (!String.IsNullOrEmpty(arguments)) { + p.StartInfo.Arguments = arguments; + } + + p.Start(); + + if (p.StartInfo.RedirectStandardOutput) { + standardOutput = p.StandardOutput.ReadToEnd(); + } else { + standardOutput = String.Empty; + } + + p.WaitForExit(); + + return p.ExitCode; + } + + public bool Error(IRequest request, ErrorCategory category, string targetObjectValue, string messageText, params object[] args) { + return request.Error(messageText, category.ToString(), targetObjectValue, request.FormatMessageString(messageText, args)); + } + + public bool Warning(IRequest request, string messageText, params object[] args) { + return request.Warning(request.FormatMessageString(messageText, args)); + } + + public bool Message(IRequest request, string messageText, params object[] args) { + return request.Message(request.FormatMessageString(messageText, args)); + } + + public bool Verbose(IRequest request, string messageText, params object[] args) { + return request.Verbose(request.FormatMessageString(messageText, args)); + } + + public bool Debug(IRequest request, string messageText, params object[] args) { + return request.Debug(request.FormatMessageString(messageText, args)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Implementation/Request.cs b/src/Microsoft.PackageManagement/Implementation/Request.cs new file mode 100644 index 000000000..7a5494980 --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/Request.cs @@ -0,0 +1,266 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Implementation { + using System; + using System.Net; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Security; + using Internal.Api; + + public abstract class Request : IRequest { + #region core-apis + + public abstract IPackageManagementService PackageManagementService {get;} + + public abstract IProviderServices ProviderServices {get;} + + #endregion + + #region copy host-apis + + /* Synced/Generated code =================================================== */ + public abstract bool IsCanceled { get; } + + public abstract string GetMessageString(string messageText, string defaultText); + + public abstract bool Warning(string messageText); + + public abstract bool Error(string id, string category, string targetObjectValue, string messageText); + + public abstract bool Message(string messageText); + + public abstract bool Verbose(string messageText); + + public abstract bool Debug(string messageText); + + public abstract int StartProgress(int parentActivityId, string messageText); + + public abstract bool Progress(int activityId, int progressPercentage, string messageText); + + public abstract bool Progress(string activity, string messageText, int activityId, int progressPercentage, int secondsRemaining, string currentOperation, int parentActivityId, bool completed); + + public abstract bool CompleteProgress(int activityId, bool isSuccessful); + + /// + /// Used by a provider to request what metadata keys were passed from the user + /// + /// + public abstract IEnumerable OptionKeys {get;} + + /// + /// + /// + /// + public abstract IEnumerable GetOptionValues(string key); + + public abstract IEnumerable Sources {get;} + + public abstract IWebProxy WebProxy { get; } + + public abstract string CredentialUsername { get; } + + public abstract SecureString CredentialPassword { get; } + + public abstract bool ShouldBootstrapProvider(string requestor, string providerName, string providerVersion, string providerType, string location, string destination); + + public abstract bool ShouldContinueWithUntrustedPackageSource(string package, string packageSource); + + public abstract bool ShouldContinue(string query, string caption, ref bool yesToAll, ref bool noToAll); + + public abstract bool ShouldContinue(string query, string caption); + + public abstract bool AskPermission(string permission); + + public abstract bool IsInteractive {get;} + + public abstract int CallCount {get;} + + #endregion + + #region copy response-apis + + /* Synced/Generated code =================================================== */ + + /// + /// Used by a provider to return fields for a SoftwareIdentity. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public abstract string YieldSoftwareIdentity(string fastPath, string name, string version, string versionScheme, string summary, string source, string searchKey, string fullPath, string packageFileName); + + public abstract string YieldSoftwareIdentityXml(string xmlSwidTag, bool commitImmediately); + + public abstract bool IsSwidTagXml(string xmlSwidTag); + + public abstract string AddTagId(string tagId); + + public abstract string AddCulture(string xmlLang); + + public abstract string AddMetadata(string name, string value); + + public abstract string AddMetadata(string elementPath, string name, string value); + + public abstract string AddMetadata(string elementPath, Uri @namespace, string name, string value); + + public abstract string AddMeta(string elementPath); + + public abstract string AddEntity(string name, string regid, string role, string thumbprint); + + public abstract string AddLink(Uri referenceUri, string relationship, string mediaType, string ownership, string use, string appliesToMedia, string artifact); + + public abstract string AddDependency(string providerName, string packageName, string version, string source, string appliesTo); + + public abstract string AddPayload(); + + public abstract string AddEvidence(DateTime date, string deviceId); + + public abstract string AddDirectory(string elementPath, string directoryName, string location, string root, bool isKey); + + public abstract string AddFile(string elementPath, string fileName, string location, string root, bool isKey, long size, string version); + + public abstract string AddProcess(string elementPath, string processName, int pid); + + public abstract string AddResource(string elementPath, string type); + + /// + /// Used by a provider to return fields for a package source (repository) + /// + /// + /// + /// + /// + /// + /// + public abstract bool YieldPackageSource(string name, string location, bool isTrusted, bool isRegistered, bool isValidated); + + /// + /// Used by a provider to return the fields for a Metadata Definition + /// The cmdlets can use this to supply tab-completion for metadata to the user. + /// + /// the provider-defined name of the option + /// one of ['string','int','path','switch'] + /// if the parameter is mandatory + /// + public abstract bool YieldDynamicOption(string name, string expectedType, bool isRequired); + + public bool YieldDynamicOption(string name, string expectedType, bool isRequired, IEnumerable permittedValues) { + return YieldDynamicOption(name, expectedType, isRequired) && (permittedValues ?? Enumerable.Empty()).All(each => YieldKeyValuePair(name, each)); + } + + public abstract bool YieldKeyValuePair(string key, string value); + + public abstract bool YieldValue(string value); + + #endregion + + + #region declare Request-implementation + /// + /// Yield values in a dictionary as key/value pairs. (one pair for each value in each key) + /// + /// + /// + public bool Yield(Dictionary dictionary) { + if( dictionary != null ) { + return dictionary.All(Yield); + } + return true; + } + + public bool Yield(KeyValuePair pair) { + if (pair.Value.Length == 0) { + return YieldKeyValuePair(pair.Key, null); + } + return pair.Value.All(each => YieldKeyValuePair(pair.Key, each)); + } + + public bool Error(ErrorCategory category, string targetObjectValue, string messageText, params object[] args) { + return Error(messageText, category.ToString(), targetObjectValue, FormatMessageString(messageText, args)); + } + + public bool Warning(string messageText, params object[] args) { + return Warning(FormatMessageString(messageText, args)); + } + + public bool Message(string messageText, params object[] args) { + return Message(FormatMessageString(messageText, args)); + } + + public bool Verbose(string messageText, params object[] args) { + return Verbose(FormatMessageString(messageText, args)); + } + + public bool Debug(string messageText, params object[] args) { + return Debug(FormatMessageString(messageText, args)); + } + + public int StartProgress(int parentActivityId, string messageText, params object[] args) { + return StartProgress(parentActivityId, FormatMessageString(messageText, args)); + } + + public bool Progress(string activity, string messageText, int activityId, int progressPercentage, int secondsRemaining, string currentOperation, int parentActivityId, bool completed, params object[] args) + { + return Progress(activity, FormatMessageString(messageText, args), activityId, progressPercentage, secondsRemaining, currentOperation, parentActivityId, completed); + } + + public bool Progress(int activityId, int progressPercentage, string messageText, params object[] args) { + return Progress(activityId, progressPercentage, FormatMessageString(messageText, args)); + } + + public string GetOptionValue(string name) { + // get the value from the request + return (GetOptionValues(name) ?? Enumerable.Empty()).LastOrDefault(); + } + + private static string FixMeFormat(string formatString, object[] args) { + if (args == null || args.Length == 0) { + // not really any args, and not really expectng any + return formatString.Replace('{', '\u00ab').Replace('}', '\u00bb'); + } + return args.Aggregate(formatString.Replace('{', '\u00ab').Replace('}', '\u00bb'), (current, arg) => current + string.Format(CultureInfo.CurrentCulture, " \u00ab{0}\u00bb", arg)); + } + + #endregion + + protected string FormatMessageString(string messageText, params object[] args) { + if (string.IsNullOrWhiteSpace(messageText)) { + return string.Empty; + } + + if (messageText.IndexOf(Constants.MSGPrefix, StringComparison.CurrentCultureIgnoreCase) == 0) { + // check with the caller first, then with the local resources, and fallback to using the messageText itself. + messageText = GetMessageString(messageText.Substring(Constants.MSGPrefix.Length),messageText) ?? messageText; + } + + // if it doesn't look like we have the correct number of parameters + // let's return a fix-me-format string. + var c = messageText.ToCharArray().Where(each => each == '{').Count(); + if (c < args.Length) { + return FixMeFormat(messageText, args); + } + return string.Format(CultureInfo.CurrentCulture, messageText, args); + } + } +} diff --git a/src/Microsoft.PackageManagement/Implementation/RequestObject.cs b/src/Microsoft.PackageManagement/Implementation/RequestObject.cs new file mode 100644 index 000000000..c3323d0bf --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/RequestObject.cs @@ -0,0 +1,428 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Implementation { + using System; + using System.Collections.Generic; + using System.Security; + using System.Threading; + using System.Threading.Tasks; + using Internal.Api; + using Resources; + using Utility.Async; + using Utility.Extensions; + using Microsoft.PackageManagement.Internal.Packaging; + using System.Net; + + public abstract class RequestObject : AsyncAction, IRequest, IHostApi { + private static int _c; + protected Action _action; + private IHostApi _hostApi; + protected Task _invocationTask; + protected readonly ProviderBase Provider; + + internal RequestObject(ProviderBase provider, IHostApi hostApi, Action action) { + // construct request object + _hostApi = hostApi; + Provider = provider; + _action = action; + } + + internal RequestObject(ProviderBase provider, IHostApi hostApi) { + // construct request object + _hostApi = hostApi; + Provider = provider; + } + + private bool CanCallHost { + get { + if (IsCompleted || IsAborted) { + return false; + } + Activity(); + return _hostApi != null; + } + } + + public override bool IsCanceled { + get { + return base.IsCanceled || (CanCallHost && _hostApi.IsCanceled); + } + } + + public override void WarnBeforeResponsivenessCancellation() { + if (CanCallHost) { + _hostApi.Warning((GetMessageString(Constants.Messages.ProviderNotResponsive, null) ?? "Provider '{0}' is not respecting the responsiveness threshold in a timely fashion; Canceling request.").format(Provider.ProviderName)); + } + } + public override void WarnBeforeTimeoutCancellation() { + if (CanCallHost) { + _hostApi.Warning((GetMessageString(Constants.Messages.ProviderTimeoutExceeded, null) ?? "Provider '{0}' is not completing the request in the time allowed; Canceling request.").format(Provider.ProviderName)); + } + } + + + protected void InvokeImpl() { + _invocationTask = Task.Factory.StartNew(() => { + _invocationThread = Thread.CurrentThread; + _invocationThread.Name = Provider.ProviderName + ":" + _c++; + + try { + _action(this); +#if !CORECLR + } catch (ThreadAbortException) { +#if DEEP_DEBUG + Console.WriteLine("Thread Aborted for {0} : {1}", _invocationThread.Name, DateTime.Now.Subtract(_callStart).TotalSeconds); +#endif + Thread.ResetAbort(); +#endif + } catch (Exception e) { + e.Dump(); + } finally { + try { + Complete(); + } + catch (Exception e) + { + e.Dump(); + } + } + }, _cancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); // .ContinueWith(antecedent => Complete()); + + // start thread, call function + StartCall(); + } + + #region HostApi Wrapper + public string DropMsgPrefix(string messageText) { + if (string.IsNullOrWhiteSpace(messageText)) { + return messageText; + } + return messageText.StartsWith("MSG:", StringComparison.OrdinalIgnoreCase) ? messageText.Substring(4) : messageText; + } + + public string GetMessageString(string messageText, string defaultText) { + if (CanCallHost) { + if (string.IsNullOrWhiteSpace(defaultText) || defaultText.StartsWith("MSG:", StringComparison.OrdinalIgnoreCase)) { + defaultText = Messages.ResourceManager.GetString(DropMsgPrefix(messageText)); + } + + return _hostApi.GetMessageString(messageText, defaultText); + } + return null; + } + + public bool Warning(string messageText) { + if (CanCallHost) { + return _hostApi.Warning(GetMessageString(messageText, null) ?? messageText); + } + return true; + } + + public bool Error(string id, string category, string targetObjectValue, string messageText) { + if (CanCallHost) { + return _hostApi.Error(id, category, targetObjectValue, GetMessageString(messageText, null) ?? messageText); + } + return true; + } + + public bool Message(string messageText) { + if (CanCallHost) { + return _hostApi.Message(GetMessageString(messageText, null) ?? messageText); + } + return true; + } + + public bool Verbose(string messageText) { + if (CanCallHost) { + return _hostApi.Verbose(GetMessageString(messageText, null) ?? messageText); + } + return true; + } + + public bool Debug(string messageText) { + if (CanCallHost) { + return _hostApi.Debug(GetMessageString(messageText, null) ?? messageText); + } + return true; + } + + public int StartProgress(int parentActivityId, string messageText) { + if (CanCallHost) { + return _hostApi.StartProgress(parentActivityId, messageText); + } + return 0; + } + + public bool Progress(int activityId, int progressPercentage, string messageText) { + if (CanCallHost) { + return _hostApi.Progress(activityId, progressPercentage, GetMessageString(messageText, null) ?? messageText); + } + return true; + } + + public bool Progress(string activity, string status, int id, int percentcomplete, int secondsremaining, string currentoperation, int parentid, bool completed) + { + if (CanCallHost) + { + return _hostApi.Progress(activity, status, id, percentcomplete, secondsremaining, currentoperation, parentid, completed); + } + + return true; + } + + public bool CompleteProgress(int activityId, bool isSuccessful) { + if (CanCallHost) { + return _hostApi.CompleteProgress(activityId, isSuccessful); + } + return true; + } + + public IEnumerable OptionKeys { + get { + if (CanCallHost) { + return _hostApi.OptionKeys; + } + return new string[0]; + } + } + + public IEnumerable GetOptionValues(string key) { + if (CanCallHost) { + return _hostApi.GetOptionValues(key); + } + return new string[0]; + } + + public IEnumerable Sources { + get { + if (CanCallHost) { + return _hostApi.Sources; + } + return new string[0]; + } + } + + public IWebProxy WebProxy + { + get + { + return CanCallHost ? _hostApi.WebProxy : null; + } + } + + public string CredentialUsername { + get { + return CanCallHost ? _hostApi.CredentialUsername : null; + } + } + + public SecureString CredentialPassword { + get { + return CanCallHost ? _hostApi.CredentialPassword : null; + } + } + + public bool ShouldBootstrapProvider(string requestor, string providerName, string providerVersion, string providerType, string location, string destination) { + if (CanCallHost) { + return _hostApi.ShouldBootstrapProvider(requestor, providerName, providerVersion, providerType, location, destination); + } + return false; + } + + public bool ShouldContinueWithUntrustedPackageSource(string package, string packageSource) { + if (CanCallHost) { + return _hostApi.ShouldContinueWithUntrustedPackageSource(package, packageSource); + } + return false; + } + + public bool ShouldContinue(string query, string caption, ref bool yesToAll, ref bool noToAll) { + if (CanCallHost) { + return _hostApi.ShouldContinue(query, caption, ref yesToAll, ref noToAll); + } + return false; + } + + public bool ShouldContinue(string query, string caption) + { + if (CanCallHost) { + return _hostApi.ShouldContinue(query, caption); + } + return false; + } + public bool AskPermission(string permission) { + if (CanCallHost) { + return _hostApi.AskPermission(permission); + } + return false; + } + + public bool IsInteractive { + get { + if (CanCallHost) { + return _hostApi.IsInteractive; + } + return false; + } + } + + public int CallCount { + get { + if (CanCallHost) { + return _hostApi.CallCount; + } + return 0; + } + } + + #endregion + + #region CoreApi implementation + + public IPackageManagementService PackageManagementService { + get { + Activity(); + return PackageManager.Instance; + } + } + + public IProviderServices ProviderServices { + get { + Activity(); + return ProviderServicesImpl.Instance; + } + } + + + + #endregion + + #region response api implementation + + public virtual string YieldSoftwareIdentity(string fastPath, string name, string version, string versionScheme, string summary, string source, string searchKey, string fullPath, string packageFileName) { + Debug("Unexpected call to YieldSoftwareIdentity in RequestObject"); + return null; + } + + public virtual string YieldSoftwareIdentityXml(string xmlSwidTag, bool commitImmediately) + { + Debug("Unexpected call to YieldSoftwareIdentityXml in RequestObject"); + return null; + } + + public virtual string AddMetadata(string name, string value) { + Debug("Unexpected call to AddMetaData in RequestObject"); + return null; + } + + public virtual string AddTagId(string tagId) { + Debug("Unexpected call to AddMetaData in RequestObject"); + return null; + } + + public virtual string AddCulture(string xmlLang) + { + Debug("Unexpected call to Culture in RequestObject"); + return null; + } + + public virtual string AddMetadata(string elementPath, string name, string value) { + Debug("Unexpected call to AddMetaData in RequestObject"); + return null; + } + + public virtual string AddMetadata(string elementPath, Uri @namespace, string name, string value) { + Debug("Unexpected call to AddMetaData in RequestObject"); + return null; + } + + public virtual string AddMeta(string elementPath) { + Debug("Unexpected call to AddMeta in RequestObject"); + return null; + } + + public virtual string AddPayload() { + Debug("Unexpected call to AddPayload in RequestObject"); + return null; + } + + public virtual string AddEvidence(DateTime date, string deviceId) { + Debug("Unexpected call to AddEvidence in RequestObject"); + return null; + } + public virtual string AddDirectory(string elementPath, string directoryName, string location, string root, bool isKey) { + Debug("Unexpected call to AddDirectory in RequestObject"); + return null; + } + + public virtual string AddFile(string elementPath, string fileName, string location, string root, bool isKey, long size, string version) { + Debug("Unexpected call to AddFile in RequestObject"); + return null; + } + + public virtual string AddProcess(string elementPath, string processName, int pid) { + Debug("Unexpected call to AddProcess in RequestObject"); + return null; + } + + public virtual string AddResource(string elementPath, string type) { + Debug("Unexpected call to AddResource in RequestObject"); + return null; + } + + public virtual string AddEntity(string name, string regid, string role, string thumbprint) { + Debug("Unexpected call to AddEntity in RequestObject"); + return null; + } + + public virtual string AddLink(Uri referenceUri, string relationship, string mediaType, string ownership, string use, string appliesToMedia, string artifact) { + Debug("Unexpected call to AddLink in RequestObject"); + return null; + } + + public virtual string AddDependency(string providerName, string packageName, string version, string source, string appliesTo) { + Debug("Unexpected call to AddDependency in RequestObject"); + return null; + } + + public virtual bool YieldPackageSource(string name, string location, bool isTrusted, bool isRegistered, bool isValidated) { + Console.WriteLine("SHOULD NOT GET HERE [YieldSoftwareIdentity] ================================================"); + // todo: give an actual error here + return true; // cancel + } + + public virtual bool YieldDynamicOption(string name, string expectedType, bool isRequired) { + Console.WriteLine("SHOULD NOT GET HERE [YieldSoftwareIdentity] ================================================"); + // todo: give an actual error here + return true; // cancel + } + + public virtual bool YieldKeyValuePair(string key, string value) { + Console.WriteLine("SHOULD NOT GET HERE [YieldSoftwareIdentity] ================================================"); + // todo: give an actual error here + return true; // cancel + } + + public virtual bool YieldValue(string value) { + Console.WriteLine("SHOULD NOT GET HERE [YieldSoftwareIdentity] ================================================"); + // todo: give an actual error here + return true; // cancel + } + + #endregion + + + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Implementation/SoftwareIdentityRequestObject.cs b/src/Microsoft.PackageManagement/Implementation/SoftwareIdentityRequestObject.cs new file mode 100644 index 000000000..2a4df4cc0 --- /dev/null +++ b/src/Microsoft.PackageManagement/Implementation/SoftwareIdentityRequestObject.cs @@ -0,0 +1,273 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Implementation { + using System; + using System.Globalization; + using System.Collections.Generic; + using System.Linq; + using System.Xml.Linq; + using Api; + using PackageManagement.Implementation; + using PackageManagement.Packaging; + using PackageManagement.Internal.Packaging; + using PackageManagement.Internal.Utility.Plugin; + + internal class SoftwareIdentityRequestObject : EnumerableRequestObject { + private SoftwareIdentity _currentItem; + private readonly string _status; + + public SoftwareIdentityRequestObject(ProviderBase provider, IHostApi request, Action action, string status) + : base(provider, request, action) { + _status = status; + InvokeImpl(); + } + + private void CommitCurrentItem() { + if (_currentItem != null) { + Results.Add(_currentItem); + } + _currentItem = null; + } + + public override string YieldSoftwareIdentity(string fastPath, string name, string version, string versionScheme, string summary, string source, string searchKey, string fullPath, string packageFileName) { + Activity(); + CommitCurrentItem(); + + _currentItem = new SoftwareIdentity { + FastPackageReference = fastPath, + Name = name, + Version = version, + VersionScheme = versionScheme, + Summary = summary, + Provider = (PackageProvider)Provider, + Source = source, + Status = _status, + SearchKey = searchKey, + FullPath = fullPath, + PackageFilename = packageFileName + }; + + return fastPath; + } + + public override string YieldSoftwareIdentityXml(string xmlSwidTag, bool commitImmediately=false) + { + Activity(); + CommitCurrentItem(); + + if (string.IsNullOrWhiteSpace(xmlSwidTag)) + { + return null; + } + + try + { + XDocument xdoc = XDocument.Parse(xmlSwidTag); + + if (xdoc != null && xdoc.Root != null && Swidtag.IsSwidtag(xdoc.Root)) + { + _currentItem = new SoftwareIdentity(xdoc); + if (Provider != null) { + _currentItem.Provider = (PackageProvider)Provider; + } + } + else + { + Verbose(string.Format(CultureInfo.CurrentCulture, Resources.Messages.SwidTagXmlInvalidNameSpace, xmlSwidTag, Iso19770_2.Namespace.Iso19770_2)); + } + } + catch (Exception e) + { + Verbose(e.Message); + Verbose(e.StackTrace); + } + + if (_currentItem == null) + { + Warning(string.Format(CultureInfo.CurrentCulture, Resources.Messages.SwidTagXmlNotValid)); + return null; + } + + string result = _currentItem.FastPackageReference; + + // provider author wants us to commit at once + if (commitImmediately) + { + CommitCurrentItem(); + } + + return result; + } + + /// + /// Adds a metadata key/value pair to the Swidtag. + /// + /// + /// + /// + public override string AddMetadata(string name, string value) { + Activity(); + + var swid = _currentItem; + return swid != null ? swid.AddMetadataValue(swid.FastPackageReference, name, value) : null; + } + + /// + /// Adds a tagID to the swidtag + /// + /// + /// + public override string AddTagId(string tagId) + { + Activity(); + + var swid = _currentItem; + + if (swid == null) + { + return null; + } + + // we can't modify it once add so check that there is no swidtag + if (string.IsNullOrWhiteSpace(swid.TagId)) + { + swid.TagId = tagId; + } + + return swid.TagId; + } + + public override string AddCulture(string xmlLang) + { + Activity(); + + var swid = _currentItem; + + if (swid == null) + { + return null; + } + + // we can't modify it once add, so check that there is no swidtag + if (string.IsNullOrWhiteSpace(swid.Culture)) + { + swid.Culture = xmlLang; + } + + return swid.Culture; + } + + /// + /// Adds an attribute to a 'Meta' object. If called for a Swidtag or Entity, it implicitly adds a child Meta object. + /// Any other elementPath is an error and will be ignored. + /// + /// + /// the string that represents one of : + /// a Swidtag (the fastPackageReference) (passing null as the elementPath will default to the swidtag) + /// an Entity (the result gained from an YieldEntity(...) call ) + /// a Meta object (the result from previously calling AddMetadataValue(...) ) + /// + /// the name of the attribute to add + /// the value of the attribute to add + /// + public override string AddMetadata(string elementPath, string name, string value) { + Activity(); + + var swid = _currentItem; + return swid != null ? swid.AddMetadataValue(elementPath, name, value) : null; + } + + public override string AddMetadata(string elementPath, Uri @namespace, string name, string value) { + Activity(); + + var swid = _currentItem; + if (swid == null || string.IsNullOrWhiteSpace(elementPath)) { + return null; + } + + return swid.AddMetadataValue(elementPath, @namespace, name, value); + } + + public override string AddMeta(string elementPath) { + Activity(); + + var swid = _currentItem; + return swid != null ? swid.AddMeta(elementPath) : null; + } + + public override string AddPayload() { + var swid = _currentItem; + return swid != null ? swid.AddPayload().ElementUniqueId : null; + } + + public override string AddEvidence(DateTime date, string deviceId) { + var swid = _currentItem; + return swid != null ? swid.AddEvidence(date, deviceId).ElementUniqueId : null; + } + + public override string AddDirectory(string elementPath, string directoryName, string location, string root, bool isKey) { + Activity(); + + var swid = _currentItem; + return swid != null ? swid.AddDirectory(elementPath, directoryName, location, root, isKey) : null; + } + + public override string AddFile(string elementPath, string fileName, string location, string root, bool isKey, long size, string version) { + Activity(); + + var swid = _currentItem; + return swid != null ? swid.AddFile(elementPath, fileName, location, root, isKey, size, version) : null; + } + + public override string AddProcess(string elementPath, string processName, int pid) { + Activity(); + + var swid = _currentItem; + return swid != null ? swid.AddProcess(elementPath, processName, pid) : null; + } + + public override string AddResource(string elementPath, string type) { + Activity(); + + var swid = _currentItem; + return swid != null ? swid.AddResource(elementPath, type) : null; + } + + public override string AddEntity(string name, string regid, string role, string thumbprint) { + Activity(); + + var swid = _currentItem; + return swid != null ? swid.AddEntity(name, regid, role, thumbprint) : null; + } + + public override string AddLink(Uri referenceUri, string relationship, string mediaType, string ownership, string use, string appliesToMedia, string artifact) { + Activity(); + + var swid = _currentItem; + return swid != null ? swid.AddLink(referenceUri, relationship, mediaType, ownership, use, appliesToMedia, artifact) : null; + } + + public override string AddDependency(string providerName, string packageName, string version, string source, string appliesTo) { + Activity(); + var swid = _currentItem; + return swid != null ? swid.AddDependency(providerName, packageName, version, source, appliesTo) : null; + } + + protected override void Complete() { + CommitCurrentItem(); + base.Complete(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/PackageManager.cs b/src/Microsoft.PackageManagement/PackageManager.cs new file mode 100644 index 000000000..fe2c84c1f --- /dev/null +++ b/src/Microsoft.PackageManagement/PackageManager.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal { + using Implementation; + + /// + /// The public interface to accessing the fetaures of the Package Management Service + /// This offers two possible methods to get the instance of the PackageManagementService. + /// If the Host is consuming the PackageManagementService by linking to this assembly, then + /// the simplest access is just to use the Instance method. + /// If the Host has dynamically loaded this assembly, then it can request a dynamically-generated + /// instance of the PackageManagementService that implements an interface of their own choosing. + /// + /// + /// + internal static class PackageManager { + private static readonly object _lockObject = new object(); + internal static IPackageManagementService _instance; + + public static IPackageManagementService Instance { + get { + lock (_lockObject) { + if (_instance == null) { + _instance = new PackageManagementService(); + } + } + return _instance; + } + } + } +} diff --git a/src/Microsoft.PackageManagement/Packaging/AttributeIndexer.cs b/src/Microsoft.PackageManagement/Packaging/AttributeIndexer.cs new file mode 100644 index 000000000..c7f513447 --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/AttributeIndexer.cs @@ -0,0 +1,77 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Xml.Linq; + using Utility.Collections; + using Utility.Extensions; + + public class AttributeIndexer { + private XElement _element; + + public AttributeIndexer(XElement element) { + _element = element; + } + + /// + /// Returns the string value of a given attribute. + /// + /// + /// the string value of a given attribute. If the attribute does not exist, returns null. + [SuppressMessage("Microsoft.Design", "CA1043:UseIntegralOrStringArgumentForIndexers", Justification = "It's a collection of attributes.")] + public string this[XName key] { + get { + if (key == null || string.IsNullOrWhiteSpace(key.LocalName)) { + return null; + } + + return _element.GetAttribute(key); + } + } + + /// + /// An enumeration of all the attributes in this element. + /// + public IEnumerable Keys { + get { + return _element.Attributes().Select(each => each.Name).ReEnumerable(); + } + } + + /// + /// An enumeration of all the attribute values in this element. + /// + public IEnumerable Values { + get { + return _element.Attributes().Select(each => each.Value).ReEnumerable(); + } + } + + /// + /// The count of the attributes in this element + /// + public int Count { + get { + return _element.Attributes().Count(); + } + } + + public override string ToString() { + return "{{{0}}}".format(Keys.Select(each => each.ToString()).JoinWithComma()); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/BaseElement.cs b/src/Microsoft.PackageManagement/Packaging/BaseElement.cs new file mode 100644 index 000000000..faed008a9 --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/BaseElement.cs @@ -0,0 +1,150 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System; + using System.Linq; + using System.Xml; + using System.Xml.Linq; + using Utility.Extensions; + + /// + /// The base element that is common to all elements in a Swidtag. + /// Swidtag classes are intended to be constructed, but are not mutatable + /// (ie, can't be created and have values modified or removed) + /// + public class BaseElement { + private AttributeIndexer _attributeIndexer; + private string _uniqueId; + protected internal XElement Element; + + protected internal BaseElement(XElement element) { + Element = element; + } + + protected internal BaseElement(XDocument element) { + if (element != null) { + Element = element.Root; + } + } + + internal string ElementUniqueId { + get { + return _uniqueId ?? (_uniqueId = PathToElement(Element)); + } + } + + /// + /// All Swidtag elements can explicity support the xml:lang attribute. + /// + public string Culture { + get { + var xmlLang = GetAttribute(Iso19770_2.Attributes.XmlLang); + if (string.IsNullOrWhiteSpace(xmlLang)) + { + xmlLang = GetAttribute(Iso19770_2.Attributes.Lang); + } + return xmlLang; + } + internal set { + AddAttribute(Iso19770_2.Attributes.XmlLang, value); + } + } + + public AttributeIndexer Attributes { + get { + return _attributeIndexer ?? (_attributeIndexer = new AttributeIndexer(Element)); + } + } + + protected static bool IsMetaElement(XElement element) { + return Iso19770_2.Elements.MetaElements.Contains(element.Name); + } + + private static string PathToElement(string parent, XElement element) { + return "{0}/{1}[{2}]".format(parent, element.Name.LocalName, element.ElementsBeforeSelf(element.Name).Count() + 1); + } + + protected static string PathToElement(XElement element) { + if (element.Parent != null) { + return PathToElement(PathToElement(element.Parent), element); + } + return "/" + element.Name.LocalName; + } + + private static XElement FindChildElementViaPath(string parentPath, XElement element, string elementId) { + foreach (var each in element.Elements()) { + var eId = PathToElement(parentPath, each); + if (elementId == eId) { + return each; + } + + if (elementId.IndexOf(eId, StringComparison.CurrentCulture) == 0) + { + return FindChildElementViaPath(eId, each, elementId); + } + } + return null; + } + + protected virtual XElement FindElementWithUniqueId(string elementId) { + if (elementId == ElementUniqueId) { + return Element; + } + return FindChildElementViaPath(ElementUniqueId, Element, elementId); + } + + /// + /// Internal method to gain access to attributes. + /// Returns Null if the attribute is not present. + /// + /// + /// + internal string GetAttribute(XName attribute) { + return Element.GetAttribute(attribute); + } + + /// + /// Adds an attribute to this element. + /// + /// name of the attribute + /// value of the attribute + /// The current element (enables fluent usage) + internal XElement AddAttribute(XName attribute, string value) { + return Element.AddAttribute(attribute, value); + } + + /// + /// Adds a new element as a child to the current element + /// + /// the swid element to add + /// The newly added element + internal T AddElement(T swidElement) where T : BaseElement { + Element.Add(swidElement.Element); + return swidElement; + } + + /// + /// Checks if the element contains an attribute with the given key + /// + /// the key to find + /// True if the element contains the key. + public bool ContainsKey(XName key) { + if (key == null || string.IsNullOrWhiteSpace(key.LocalName)) { + return false; + } + return Element.Attribute(key) != null; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/Directory.cs b/src/Microsoft.PackageManagement/Packaging/Directory.cs new file mode 100644 index 000000000..0bf26abf0 --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/Directory.cs @@ -0,0 +1,75 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Xml.Linq; + using Utility.Collections; + + /// + /// Provides the ability to apply a directory structure to the files + /// defined in a Payload or Evidence element. + /// A Directory element allows files or directories to be + /// defined in the file structure. + /// + public class Directory : FilesystemItem { + internal Directory(XElement element) + : base(element) { + if (element.Name != Iso19770_2.Elements.Directory) { + throw new ArgumentException("Element is not of type 'Directory'", "element"); + } + } + + internal Directory(string name) + : base(new XElement(Iso19770_2.Elements.Directory)) { + Name = name; + } + + /// + /// An enumeration of the child directories in the element. + /// + public IEnumerable Directories { + get { + return Element.Elements(Iso19770_2.Elements.Directory).Select(each => new Directory(each)).ReEnumerable(); + } + } + + /// + /// An enumeration of the Files contained in the element + /// + public IEnumerable Files { + get { + return Element.Elements(Iso19770_2.Elements.File).Select(each => new File(each)).ReEnumerable(); + } + } + + /// + /// Adds a child directory element. + /// + /// The newly created directory element + internal Directory AddDirectory(string directoryName) { + return AddElement(new Directory(directoryName)); + } + + /// + /// Adds a file element. + /// + /// The newly created file element + internal File AddFile(string filename) { + return AddElement(new File(filename)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/DynamicOption.cs b/src/Microsoft.PackageManagement/Packaging/DynamicOption.cs new file mode 100644 index 000000000..c53373d42 --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/DynamicOption.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + + public class DynamicOption { + private IEnumerable _values; + public string ProviderName {get; set;} + public OptionCategory Category {get; internal set;} + public string Name {get; internal set;} + [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "Type is OK for a cmdlet parameter")] + public OptionType Type { get; internal set;} + public bool IsRequired {get; internal set;} + + public IEnumerable PossibleValues { + get { + return _values ?? Enumerable.Empty(); + } + internal set { + _values = value; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/Entity.cs b/src/Microsoft.PackageManagement/Packaging/Entity.cs new file mode 100644 index 000000000..562d43746 --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/Entity.cs @@ -0,0 +1,157 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Xml.Linq; + using Utility.Collections; + using Utility.Extensions; + + /// + /// From the schema: + /// Specifies the organizations related to the software component + /// referenced by this SWID tag. + /// This has a minOccurs of 1 because the spec declares that + /// you must have at least a Entity with role='tagCreator' + /// + public class Entity : BaseElement { + /// + /// Construct an Entity object tied to an existing element in a document + /// + /// + internal Entity(XElement element) : base(element) { + if (element.Name != Iso19770_2.Elements.Entity) { + throw new ArgumentException("Element is not of type 'Entity'", "element"); + } + } + + internal Entity(string name, string regId, string role) + : base(new XElement(Iso19770_2.Elements.Entity)) { + Name = name; + RegId = string.IsNullOrWhiteSpace(regId) ? "invalid.unavailable" : regId; + AddRole(role); + } + + /// + /// The name of the organization claiming a particular role in the SWIDtag. + /// + public string Name { + get { + return GetAttribute(Iso19770_2.Attributes.Name); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Name, value); + } + } + + /// + /// An enumeration of the Roles for a given entity. + /// From the schema: + /// The relationship between this organization and this tag e.g. tag, + /// softwareCreator, licensor, tagCreator, etc. The role of + /// tagCreator is required for every SWID tag. + /// Role may include any role value, but the pre-defined roles + /// include: aggregator, distributor, licensor, softwareCreator, + /// tagCreator + /// Other roles will be defined as the market uses the SWID tags. + /// + public IEnumerable Roles { + get { + var attr = GetAttribute(Iso19770_2.Attributes.Role.LocalName); + if (attr != null) { + return attr.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries); + } + return Enumerable.Empty(); + } + } + + public string Role { + get { + return GetAttribute(Iso19770_2.Attributes.Role.LocalName); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Role, value); + } + } + + /// + /// this value provides a hexadecimal string that contains a hash + /// (or thumbprint) of the signing entities certificate. + /// + public string Thumbprint { + get { + return GetAttribute(Iso19770_2.Attributes.Thumbprint.LocalName); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Thumbprint, value); + } + } + + /// + /// The regid of the organization. If the regid is unknown, the + /// value "invalid.unavailable" is assumed by default (see + /// RFC 6761 for more details on the default value). + /// + public string RegId { + get { + return GetAttribute(Iso19770_2.Attributes.RegId.LocalName); + } + internal set { + AddAttribute(Iso19770_2.Attributes.RegId, value); + } + } + + /// + /// An enumeration of nested metadata for the Entity + /// + public IEnumerable Meta { + get { + return Element.Elements(Iso19770_2.Elements.Meta).Select(each => new Meta(each)).ReEnumerable(); + } + } + + /// + /// adds a role to the element. + /// + /// the role to add + internal void AddRole(string role) { + if (string.IsNullOrWhiteSpace(role)) { + role = string.Empty; + } + + var attr = GetAttribute(Iso19770_2.Attributes.Role.LocalName); + if (attr != null) { + var roles = attr.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries).ConcatSingleItem(role).Distinct(); + role = roles.JoinWith(" "); + } + + // if there isn't an actual role added; set it to 'unknown' + if (string.IsNullOrWhiteSpace(role)) { + role = "unknown"; + } + + Element.SetAttributeValue(Iso19770_2.Attributes.Role, role.Trim()); + } + + /// + /// Adds a nested metadata element to the entity. + /// + /// + internal Meta AddMeta() { + return AddElement(new Meta()); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/Evidence.cs b/src/Microsoft.PackageManagement/Packaging/Evidence.cs new file mode 100644 index 000000000..63d6d7dbc --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/Evidence.cs @@ -0,0 +1,78 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System; + using System.Globalization; + using System.Xml; + using System.Xml.Linq; + + /// + /// From the schema: + /// The element is used to provide results from a scan of a system + /// where software that does not have a SWID tag is discovered. + /// This information is not provided by the software creator, and + /// is instead created when a system is being scanned and the + /// evidence for why software is believed to be installed on the + /// device is provided in the Evidence element. + /// + public class Evidence : ResourceCollection { + internal Evidence(XElement element) + : base(element) { + if (element.Name != Iso19770_2.Elements.Evidence) { + throw new ArgumentException("Element is not of type 'Evidence'", "element"); + } + } + + internal Evidence() + : base(new XElement(Iso19770_2.Elements.Evidence)) { + } + + /// + /// Date the evidence was gathered. + /// + public DateTime? Date { + get { + var v = GetAttribute(Iso19770_2.Attributes.Date); + if (v != null) { + try { + return XmlConvert.ToDateTime(v, XmlDateTimeSerializationMode.Utc); + } catch { + } + } + return null; + } + internal set { + if (value == null) { + return; + } + var v = (DateTime)value; + + AddAttribute(Iso19770_2.Attributes.Date, v.ToUniversalTime().ToString("o", CultureInfo.CurrentCulture)); + } + } + + /// + /// Identifier for the device the evidence was gathered from. + /// + public string DeviceId { + get { + return GetAttribute(Iso19770_2.Attributes.DeviceId); + } + internal set { + AddAttribute(Iso19770_2.Attributes.DeviceId, value); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/File.cs b/src/Microsoft.PackageManagement/Packaging/File.cs new file mode 100644 index 000000000..4d992b1e1 --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/File.cs @@ -0,0 +1,70 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System; + using System.Xml.Linq; + + /// + /// Represents an individual file + /// + public class File : FilesystemItem { + internal File(XElement element) + : base(element) { + if (element.Name != Iso19770_2.Elements.File) { + throw new ArgumentException("Element is not of type 'File'", "element"); + } + } + + internal File(string filename) + : base(new XElement(Iso19770_2.Elements.File)) { + Name = filename; + } + + /// + /// From the swidtag schema: + /// The file size in bytes of the file + /// + public long? Size { + get { + var sz = GetAttribute(Iso19770_2.Attributes.Size); + if (sz != null) { + long result; + if (Int64.TryParse(sz, out result)) { + return result; + } + } + return null; + } + internal set { + if (value != null) { + AddAttribute(Iso19770_2.Attributes.Size, value.ToString()); + } + } + } + + /// + /// From the swidtag schema: + /// The file version + /// + public string Version { + get { + return GetAttribute(Iso19770_2.Attributes.Version); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Version, value); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/FilesystemItem.cs b/src/Microsoft.PackageManagement/Packaging/FilesystemItem.cs new file mode 100644 index 000000000..4460a5b86 --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/FilesystemItem.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System.Xml.Linq; + using Utility.Extensions; + + /// + /// Represents an individual file or directory. + /// + public class FilesystemItem : Meta { + internal FilesystemItem(XElement element) + : base(element) { + } + + /// + /// From the swidtag schema: + /// Files that are considered important or required for the use of + /// a software component. Typical key files would be those which, + /// if not available on a system, would cause the software not to + /// execute. + /// Key files will typically be used to validate that software + /// referenced by the SWID tag is actually installed on a specific + /// computing device + /// + public bool? IsKey { + get { + return GetAttribute(Iso19770_2.Attributes.Key).IsTruePreserveNull(); + } + internal set { + if (value != null) { + AddAttribute(Iso19770_2.Attributes.Key, value.ToString()); + } + } + } + + /// + /// From the swidtag schema: + /// The directory or location where a file was found or can expected + /// to be located. does not include the filename itself. This can + /// be relative path from the 'root' attribute. + /// + public string Location { + get { + return GetAttribute(Iso19770_2.Attributes.Location); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Location, value); + } + } + + /// + /// From the swidtag schema: + /// The filename without any path characters + /// + public string Name { + get { + return GetAttribute(Iso19770_2.Attributes.Name); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Name, value); + } + } + + /// + /// From the swidtag schema: + /// A system-specific root folder that the 'location' + /// attribute is an offset from. If this is not specified + /// the assumption is the 'root' is the same folder as + /// the location of the SWIDTAG. + /// + public string Root { + get { + return GetAttribute(Iso19770_2.Attributes.Root); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Root, value); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/Iso19770_2.cs b/src/Microsoft.PackageManagement/Packaging/Iso19770_2.cs new file mode 100644 index 000000000..d5301c596 --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/Iso19770_2.cs @@ -0,0 +1,260 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System; + using System.Linq; + using System.Xml.Linq; + using Utility.Extensions; + + internal static class Iso19770_2 { + + internal static XAttribute SwidtagNamespace { + get { + return new XAttribute(XNamespace.Xmlns + "swid", Namespace.Iso19770_2); + } + } + + /// + /// Gets the attribute value for a given element. + /// + /// the element that possesses the attribute + /// the attribute to find + /// the string value of the element. Returns null if the element or attribute does not exist. + internal static string GetAttribute(this XElement element, XName attribute) { + if (element == null || attribute == null || string.IsNullOrWhiteSpace(attribute.ToString())) { + return null; + } + + XAttribute result; + + // no name space, just check local name + if (string.IsNullOrWhiteSpace(attribute.Namespace.NamespaceName)) + { + result = element.Attributes().Where(attr => attr != null && string.Equals(attr.Name.LocalName, attribute.LocalName, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); + } + else + { + result = element.Attribute(attribute); + } + + return result == null ? null : result.Value; + } + + /// + /// Adds a new attribute to the element + /// Does not permit modification of an existing attribute. + /// Does not add empty or null attributes or values. + /// + /// The element to add the attribute to + /// The attribute to add + /// the value of the attribute to add + /// The element passed in. (Permits fluent usage) + internal static XElement AddAttribute(this XElement element, XName attribute, string value) { + if (element == null) { + return null; + } + + // we quietly ignore attempts to add empty data or attributes. + if (string.IsNullOrWhiteSpace(value) || attribute == null || string.IsNullOrWhiteSpace(attribute.ToString())) { + return element; + } + + // Swidtag attributes can be added but not changed -- if it already exists, that's not permitted. + var current = element.GetAttribute(attribute); + if (!string.IsNullOrWhiteSpace(current)) { + if (value != current) { + throw new Exception("Attempt to change Attribute '{0}' present in element '{1}'".format(attribute.LocalName, element.Name.LocalName)); + } + + // if the value was set to that already, don't worry about it. + return element; + } + + element.SetAttributeValue(attribute, value); + + return element; + } + + internal static class Attributes { + internal static readonly XName Name = "name"; + internal static readonly XName Patch = "patch"; + internal static readonly XName Media = "media"; + internal static readonly XName Supplemental = "supplemental"; + internal static readonly XName TagVersion = "tagVersion"; + internal static readonly XName TagId = "tagId"; + internal static readonly XName Version = "version"; + internal static readonly XName VersionScheme = "versionScheme"; + internal static readonly XName Corpus = "corpus"; + internal static readonly XName Summary = "summary"; + internal static readonly XName Description = "description"; + internal static readonly XName ActivationStatus = "activationStatus"; + internal static readonly XName ChannelType = "channelType"; + internal static readonly XName ColloquialVersion = "colloquialVersion"; + internal static readonly XName Edition = "edition"; + internal static readonly XName EntitlementDataRequired = "entitlementDataRequired"; + internal static readonly XName EntitlementKey = "entitlementKey"; + internal static readonly XName Generator = "generator"; + internal static readonly XName PersistentId = "persistentId"; + internal static readonly XName Product = "product"; + internal static readonly XName ProductFamily = "productFamily"; + internal static readonly XName Revision = "revision"; + internal static readonly XName UnspscCode = "unspscCode"; + internal static readonly XName UnspscVersion = "unspscVersion"; + internal static readonly XName RegId = "regId"; + internal static readonly XName Role = "role"; + internal static readonly XName Thumbprint = "thumbprint"; + internal static readonly XName HRef = "href"; + internal static readonly XName Relationship = "rel"; + internal static readonly XName MediaType = "type"; + internal static readonly XName Ownership = "ownership"; + internal static readonly XName Use = "use"; + internal static readonly XName Artifact = "artifact"; + internal static readonly XName Type = "type"; + internal static readonly XName Key = "key"; + internal static readonly XName Root = "root"; + internal static readonly XName Location = "location"; + internal static readonly XName Size = "size"; + internal static readonly XName Pid = "pid"; + internal static readonly XName Date = "date"; + internal static readonly XName DeviceId = "deviceId"; + internal static readonly XName XmlLang = Namespace.Xml + "lang"; + internal static readonly XName Lang = "lang"; + } + + internal static class Discovery { + internal static readonly XName Name = Namespace.Discovery + "name"; + + // Feed Link Extended attributes: + internal static readonly XName MinimumName = Namespace.Discovery + "min-name"; + internal static readonly XName MaximumName = Namespace.Discovery + "max-name"; + internal static readonly XName MinimumVersion = Namespace.Discovery + "min-version"; + internal static readonly XName MaximumVersion = Namespace.Discovery + "max-version"; + internal static readonly XName Keyword = Namespace.Discovery + "keyword"; + + // Package Link Extended Attributes + internal static readonly XName Version = Namespace.Discovery + "version"; + internal static readonly XName Latest = Namespace.Discovery + "latest"; + internal static readonly XName TargetFilename = Namespace.Discovery + "targetFilename"; + internal static readonly XName Type = Namespace.Discovery + "type"; + + } + + internal static class Hash { + internal static readonly XName Hash512 = Namespace.Sha512 + "hash"; + internal static readonly XName Hash256 = Namespace.Sha256 + "hash"; + internal static readonly XName Md5 = Namespace.Md5 + "hash"; + + } + + internal static class HashAlgorithm + { + internal static readonly string Sha512 = "sha512"; + internal static readonly string Sha256 = "sha256"; + internal static readonly string Md5 = "md5"; + } + + + internal static class MediaType { + internal const string PackageReference = "application/vnd.packagemanagement-canonicalid"; + + internal const string SwidTagXml = "application/swid-tag+xml"; + internal const string SwidTagJsonLd = "application/swid-tag+json-ld"; + + internal const string MsiPackage = "application/vnd.ms.msi-package"; + internal const string MsuPackage = "application/vnd.ms.msu-package"; + + internal const string ExePackage = "application/vnd.packagemanagement.exe-package"; + internal const string ZipPackage = "application/epub+zip"; + internal const string NuGetPackage = "application/vnd.packagemanagement.nuget-package"; + internal const string ChocolateyPackage = "application/vnd.packagemanagement.chocolatey-package"; + + } + + internal static class Relationship { + internal const string Requires = "requires"; + internal const string InstallationMedia = "installationmedia"; + internal const string Component = "component"; + internal const string Supplemental = "supplemental"; + internal const string Parent = "parent"; + internal const string Ancestor = "ancestor"; + + // Package Discovery Relationships: + internal const string Feed = "feed"; // should point to a swidtag the represents a feed of packages + internal const string Package = "package"; // should point ot a swidtag that represents an installation package + } + + internal static class Role { + internal const string Aggregator = "aggregator"; + internal const string Distributor = "distributor"; + internal const string Licensor = "licensor"; + internal const string SoftwareCreator = "softwareCreator"; + internal const string Author = "author"; + internal const string Contributor = "contributor"; + internal const string Publisher = "publisher"; + internal const string TagCreator = "tagCreator"; + } + + internal static class Use { + internal const string Required = "required"; + internal const string Recommended = "recommended"; + internal const string Optional = "optional"; + } + + internal static class VersionScheme { + internal const string Alphanumeric = "alphanumeric"; + internal const string Decimal = "decimal"; + internal const string MultipartNumeric = "multipartnumeric"; + internal const string MultipartNumericPlusSuffix = "multipartnumeric+suffix"; + internal const string SemVer = "semver"; + internal const string Unknown = "unknown"; + } + + internal static class Ownership { + internal const string Abandon = "abandon"; + internal const string Private = "private"; + internal const string Shared = "shared"; + } + + internal static class Namespace { + internal static readonly XNamespace Iso19770_2 = XNamespace.Get("http://standards.iso.org/iso/19770/-2/2015/schema.xsd"); + internal static readonly XNamespace Iso19770_2_Current = XNamespace.Get("http://standards.iso.org/iso/19770/-2/2015-current/schema.xsd"); + internal static readonly XNamespace Discovery = XNamespace.Get("http://packagemanagement.org/discovery"); + internal static readonly XNamespace OneGet = XNamespace.Get("http://oneget.org/packagemanagement"); + internal static readonly XNamespace Xml = XNamespace.Get("http://www.w3.org/XML/1998/namespace"); + internal static XNamespace XmlDsig = XNamespace.Get("http://www.w3.org/2000/09/xmldsig#"); + internal static readonly XNamespace Sha512 = XNamespace.Get("http://www.w3.org/2001/04/xmlenc#sha512"); + internal static readonly XNamespace Sha256 = XNamespace.Get("http://www.w3.org/2001/04/xmlenc#sha256"); + internal static readonly XNamespace Md5 = XNamespace.Get("http://www.w3.org/2001/04/xmldsig-more#md5"); + } + + internal static class Elements { + internal static readonly XName SoftwareIdentity = Namespace.Iso19770_2 + "SoftwareIdentity"; + internal static readonly XName SoftwareIdentityCurrent = Namespace.Iso19770_2_Current + "SoftwareIdentity"; + internal static readonly XName Entity = Namespace.Iso19770_2 + "Entity"; + internal static readonly XName Link = Namespace.Iso19770_2 + "Link"; + internal static readonly XName Evidence = Namespace.Iso19770_2 + "Evidence"; + internal static readonly XName Payload = Namespace.Iso19770_2 + "Payload"; + internal static readonly XName Meta = Namespace.Iso19770_2 + "Meta"; + internal static readonly XName Directory = Namespace.Iso19770_2 + "Directory"; + internal static readonly XName File = Namespace.Iso19770_2 + "File"; + internal static readonly XName Process = Namespace.Iso19770_2 + "Process"; + internal static readonly XName Resource = Namespace.Iso19770_2 + "Resource"; + + internal static readonly XName[] MetaElements = { + Meta, Directory, File, Process, Resource + }; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/Link.cs b/src/Microsoft.PackageManagement/Packaging/Link.cs new file mode 100644 index 000000000..85e97f84f --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/Link.cs @@ -0,0 +1,168 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System; + using System.Xml.Linq; + using Utility.Extensions; + + /// + /// From the schema: + /// A reference to any another item (can include details that are + /// related to the SWID tag such as details on where software + /// downloads can be found, vulnerability database associations, + /// use rights, etc). + /// This is modeled directly to match the HTML [LINK] element; it is + /// critical for streamlining software discovery scenarios that + /// these are kept consistent. + /// + public class Link : BaseElement { + internal Link(XElement element) + : base(element) { + if (element.Name != Iso19770_2.Elements.Link) { + throw new ArgumentException("Element is not of type 'Link'", "element"); + } + } + + internal Link(Uri href, string relationship) + : base(new XElement(Iso19770_2.Elements.Link)) { + HRef = href; + Relationship = relationship; + } + + /// + /// For installation media (rel="installationmedia") - dictates the + /// canonical name for the file. + /// Items with the same artifact name should be considered mirrors + /// of each other (so download from wherever works). + /// + public string Artifact { + get { + return GetAttribute(Iso19770_2.Attributes.Artifact); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Artifact, value); + } + } + + /// + /// From the schema: + /// The link to the item being referenced. + /// The href can point to several different things, and can be any + /// of the following: + /// - a RELATIVE URI (no scheme) - which is interpreted depending on + /// context (ie, "./folder/supplemental.swidtag" ) + /// - a physical file location with any system-acceptable + /// URI scheme (ie, file:// http:// https:// ftp:// ... etc ) + /// - an URI with "swid:" as the scheme, which refers to another + /// swid by tagId. This URI would need to be resolved in the + /// context of the system by software that can lookup other + /// swidtags.( ie, "swid:2df9de35-0aff-4a86-ace6-f7dddd1ade4c" ) + /// - an URI with "swidpath:" as the scheme, which refers to another + /// swid by an XPATH query. This URI would need to be resolved in + /// the context of the system by software that can lookup other + /// swidtags, and select the appropriate one based on an XPATH + /// query. Examples: + /// swidpath://SoftwareIdentity[Entity/@regid='http://contoso.com'] + /// would retrieve all swidtags that had an entity where the + /// regid was Contoso + /// swidpath://SoftwareIdentity[Meta/@persistentId='b0c55172-38e9-4e36-be86-92206ad8eddb'] + /// would retrieve swidtags that matched a specific persistentId + /// See XPATH query standard : http://www.w3.org/TR/xpath20/ + /// + public Uri HRef { + get { + var v = GetAttribute(Iso19770_2.Attributes.HRef); + Uri result; + if (v != null && Uri.TryCreate(v, UriKind.Absolute, out result)) { + return result; + } + return null; + } + internal set { + AddAttribute(Iso19770_2.Attributes.HRef, value.ToString()); + } + } + + /// + /// An attribute defined by the W3C Media Queries Recommendation + /// (see http://www.w3.org/TR/css3-mediaqueries/). + /// A hint to the consumer of the link to what the target item is + /// applicable for. + /// + public string Media { + get { + return GetAttribute(Iso19770_2.Attributes.Media); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Media, value); + } + } + + /// + /// Determines the relative strength of ownership of the target piece of software. + /// + public string Ownership { + get { + return GetAttribute(Iso19770_2.Attributes.Ownership); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Ownership, value); + } + } + + /// + /// The relationship between this SWID and the target file. + /// + public string Relationship { + get { + return GetAttribute(Iso19770_2.Attributes.Relationship); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Relationship, value); + } + } + + /// + /// The IANA MediaType for the target file; this provides the consumer + /// with intelligence of what to expect. + /// See http://www.iana.org/assignments/media-types/media-types.xhtml + /// for more details on link type. + /// + public string MediaType { + get { + return GetAttribute(Iso19770_2.Attributes.MediaType); + } + internal set { + AddAttribute(Iso19770_2.Attributes.MediaType, value); + } + } + + /// + /// Determines if the target software is a hard requirement or not + /// + public string Use { + get { + return GetAttribute(Iso19770_2.Attributes.Use); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Use, value); + } + } + + public override string ToString() { + return "{0}:{1}".format(Relationship, HRef); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/MediaQuery.cs b/src/Microsoft.PackageManagement/Packaging/MediaQuery.cs new file mode 100644 index 000000000..6bc1f4d2e --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/MediaQuery.cs @@ -0,0 +1,401 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System; + using System.Collections; + using System.Globalization; + using System.Text.RegularExpressions; + using Microsoft.PackageManagement.Internal.Utility.Versions; + + public class MediaQuery { +#if MEDIA_QUERY_DOCUMENTATION + An expression that the document evaluator can use to determine if the + target of the link is applicable to the current platform (the host + environment) + + Used as an optimization hint to notify a system that it can + ignore something when it's not likely to be used. + + The format of this string is modeled upon the MediaQuery definition at + http://www.w3.org/TR/css3-mediaqueries/ + + This is one or more EXPRESSIONs where the items are connected + with an OPERATOR: + + media="EXPRESSION [[OPERATOR] [EXPRESSION]...]" + + EXPRESSION is processed case-insensitive and defined either : + (ENVIRONMENT) + indicates the presence of the environment + or + ([PREFIX-]ENVIRONMENT.ATTRIBUTE:VALUE) + indicates a comparison of an attribute of the environment. + + ENVIRONMENT is a text identifier that specifies any software,hardware + feature or aspect of the system the software is intended to run in. + + Common ENVIRONMENTs include (but not limited to): + linux + windows + java + powershell + ios + chipset + peripheral + + ATTRIBUTE is a property of an ENVIRONMENT with a specific value. + Common attributes include (but not limited to): + version + vendor + architecture + + PREFIX is defined as one of: + MIN # property has a minimum value of VALUE + MAX # property has a maximum value of VALUE + + if a PREFIX is not provided, then the property should equal VALUE + + OPERATOR is defined of one of: + AND + NOT + OR + + Examples: + media="(windows)" + // applies to only systems that identify themselves as 'Windows' + + media="(windows) not (windows.architecture:x64)" + // applies to only systems that identify + // themselves as windows and are not for an x64 cpu + + media="(windows) and (min-windows.version:6.1)" + // applies to systems that identify themselves as + // windows and at least version 6.1 + + media="(linux) and (linux.vendor:redhat) and (min-linux.kernelversion:3.0)" + // applies to systems that identify themselves as + // linux, made by redhat and with a kernel version of at least 3.0 + + media="(freebsd) and (min-freebsd.kernelversion:6.6)" + // applies to systems that identify themselves as + // freebsd, with a kernel version of at least 6.6 + + media="(powershell) and (min-powershell.version:3.0)" + // applies to systems that have powershell 3.0 or greater + + Properties are expected to be able to be resolved by the host + environment without having to do significant computation. + +#endif + private const RegexOptions _flags = RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture; + + internal const string Environment = "(?[a-z0-9]+)"; + internal const string Prefix = "(((?min)|(?max))-)?"; + internal const string Attribute = "(?[a-z0-9]+)"; + internal const string Value = "(?[a-z0-9\\.]+)"; + + internal static string AttributeEnvironmentValue = string.Format(CultureInfo.InvariantCulture, "(?{0}{1}\\.{2}:{3})", Prefix, Environment, Attribute, Value); + internal static string Expression = string.Format(CultureInfo.InvariantCulture, "(?{0}|{1})", Environment, AttributeEnvironmentValue); + + internal static string Tokenizer = "(?\\()|(?\\))|(?not)|(?or)|(?and)|(?[\\w:\\-\\.]+)"; + + private static readonly Regex _tokenizerRegex = new Regex(Tokenizer, _flags); + private static readonly Regex _expressionRegex = new Regex(string.Concat("^", Expression, "$"), _flags); + + private static bool ParseAndProcessExpression(Match expressionMatch, Hashtable environment) + { + string environmentName = expressionMatch.Groups["environment"].Value.ToLowerInvariant(); + + // environment name must not be null in media element + if (string.IsNullOrWhiteSpace(environmentName)) + { + return false; + } + + // current environment does not contain this environmentname element + // we assume that this is not satisfied by the current environment + if (!environment.ContainsKey(environmentName)) + { + return false; + } + + Hashtable environmentDetail = environment[environmentName] as Hashtable; + + // no details return false as no information to proceed + if (environmentDetail == null) + { + return false; + } + + string environmentAttribute = expressionMatch.Groups["attribute"].Value.ToLowerInvariant(); + + // this is the case where there is an environment attribute attached + if (!string.IsNullOrWhiteSpace(environmentAttribute)) + { + // no information about the attribute, can't verify + if (!environmentDetail.ContainsKey(environmentAttribute)) + { + return false; + } + + // this is the expected environmental value + string expectedEnvironmentValue = expressionMatch.Groups["value"].Value.ToLowerInvariant(); + + // if there is attribute then there must be value, so return false if no value + if (string.IsNullOrWhiteSpace(expectedEnvironmentValue)) + { + return false; + } + + string currentEnvironmentalValue = environmentDetail[environmentAttribute].ToString(); + Version currentVersion; + Double currentDouble; + int comparison = 0; + + // check whether currentEnvironmentalValue is a version + if (Version.TryParse(currentEnvironmentalValue, out currentVersion)) + { + // this means the value should be version as well + Version expectedVersion; + + if (!Version.TryParse(expectedEnvironmentValue, out expectedVersion)) + { + return false; + } + + comparison = ((FourPartVersion)expectedVersion).CompareTo((FourPartVersion)currentVersion); + } + else if (Double.TryParse(currentEnvironmentalValue, out currentDouble)) + { + // this means value should be double as well + Double expectedDouble; + + if (!Double.TryParse(expectedEnvironmentValue, out expectedDouble)) + { + return false; + } + + comparison = expectedDouble.CompareTo(currentDouble); + } + else + { + // just compare them as string then + comparison = string.Compare(expectedEnvironmentValue, currentEnvironmentalValue, StringComparison.OrdinalIgnoreCase); + } + + // check whether there is prefix + if (expressionMatch.Groups["prefix"].Success) + { + // if there is prefix then the value can be compared + if (string.Equals(expressionMatch.Groups["prefix"].Value, "max", System.StringComparison.OrdinalIgnoreCase)) + { + // max case which means expectedValue >= currentValue + return comparison >= 0; + } + else if (string.Equals(expressionMatch.Groups["prefix"].Value, "min", System.StringComparison.OrdinalIgnoreCase)) + { + // min case which means expectedValue <= currentValue + return comparison <= 0; + } + + // unknown prefix + return false; + } + + // no prefix then expectedValue and currentValue should be the same + return comparison == 0; + } + + // no attribute so we just have a standalone environment like "windows" + // if we reach this point then we already verify that "windows" is already in environment dictionary + return true; + } + + private static bool EvaluateMediaString(MatchCollection matches, Hashtable environment, ref int currentIndex, ref bool malformed) + { + if (currentIndex >= matches.Count) + { + malformed = true; + return false; + } + + // needs a parenthesis here at the start + Stack parenthesisStack = new Stack(); + + Match match = matches[currentIndex]; + + if (!match.Groups["open"].Success) + { + malformed = true; + return false; + } + + parenthesisStack.Push(true); + currentIndex += 1; + + bool result = false; + + while (currentIndex < matches.Count && parenthesisStack.Count != 0) + { + match = matches[currentIndex]; + currentIndex += 1; + bool isMalformed = false; + // if this boolean is set to true, then we perform cleanup and returns false + // we clean up by getting to the end of whatever opening parenthesis we open + bool cleanup = false; + + if (match.Groups["close"].Success) + { + // malformed because missing closing parenthesis + if (parenthesisStack.Count == 0) + { + malformed = true; + return false; + } + + parenthesisStack.Pop(); + } + else if (match.Groups["open"].Success) + { + parenthesisStack.Push(true); + } + else if (match.Groups["txt"].Success) + { + Match expressionMatch = _expressionRegex.Match(match.Groups["txt"].Value); + result = ParseAndProcessExpression(expressionMatch, environment); + } + else if (match.Groups["and"].Success || match.Groups["not"].Success) + { + bool evaluated = EvaluateMediaString(matches, environment, ref currentIndex, ref isMalformed); + + // malformed so just returns + if (isMalformed) + { + malformed = true; + return false; + } + + // there are 2 cases that we can proceed, either we evaluate to true with and or we evaluate to false with not. + // otherwise return false. + if ((evaluated && match.Groups["and"].Success) + || (!evaluated && match.Groups["not"].Success)) + { + result = true; + continue; + } + + // reach here then result is false. Since this is and/not, there is no need to evaluate any more so set cleanup to true + result = false; + cleanup = true; + } + else if (match.Groups["or"].Success) + { + // match the or case + bool evaluated = EvaluateMediaString(matches, environment, ref currentIndex, ref isMalformed); + + if (isMalformed) + { + malformed = true; + return false; + } + + // if true, since this is or case, set result to true and just clean up since there is no need to evaluate further + if (evaluated) + { + result = true; + cleanup = true; + } + + // otherwise we continue evaluating + } + + if (cleanup) + { + // if we reach here then we know result is false, so just keep iterating til the end parenthesis + while (parenthesisStack.Count != 0) + { + // reached the end without reaching the final closing parenthesis + if (currentIndex >= matches.Count) + { + malformed = true; + return false; + } + + match = matches[currentIndex]; + currentIndex += 1; + + if (match.Groups["open"].Success) + { + parenthesisStack.Push(true); + } + else if (match.Groups["close"].Success) + { + // malformed because missing closing parenthesis + if (parenthesisStack.Count == 0) + { + malformed = true; + return false; + } + + parenthesisStack.Pop(); + } + } + + return result; + } + } + + // if we reached this point and there is still something in the stack + // then it is malformed + if (parenthesisStack.Count != 0) + { + malformed = true; + return false; + } + + return result; + } + + /// + /// Parse a media query and checks whether the hashtable environment supplied + /// satisfies the query. See documentation above for more details. + /// + /// + /// + /// + public static bool IsApplicable(string mediaQuery, Hashtable environment) { + // no media query given or empty environment table then don't check + if (string.IsNullOrWhiteSpace(mediaQuery) || environment == null || environment.Count == 0) + { + return true; + } + + // append ( and ) to make it easier to parse + mediaQuery = string.Concat("(", mediaQuery, ")"); + + int currentIndex = 0; + + bool isMalformed = false; + + // if we evaluated to true and the string is not malformed, return true. + if (EvaluateMediaString(_tokenizerRegex.Matches(mediaQuery.Trim()), environment, ref currentIndex, ref isMalformed) && !isMalformed) + { + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/Meta.cs b/src/Microsoft.PackageManagement/Packaging/Meta.cs new file mode 100644 index 000000000..bf25ee134 --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/Meta.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System.Xml.Linq; + + /// + /// An intermediate base class for elements in the swidtag that support + /// arbitrary attributes in the default namespace. + /// From the swidtag schema: + /// An open-ended collection of key/value data related to this SWID. + /// Permits any user-defined attributes in Meta tags + /// + public class Meta : BaseElement { + internal Meta(XElement element) + : base(element) { + // we don't do an Element type validation here, since this will get called from + // child classes too. + // and that's ok, because Meta doesn't directly have any predefined fields anyway. + } + + internal Meta() + : base(new XElement(Iso19770_2.Elements.Meta)) { + } + + public string this[string key] { + get { + return Attributes[key]; + } + } + + public override string ToString() { + return Attributes != null ? Attributes.ToString() : string.Empty; + } + + /// + /// Determines if the element contains an attribute with the given name. + /// + /// the attribute to find + /// True, if the element contains the attribute. + public bool ContainsKey(string key) { + if (string.IsNullOrWhiteSpace(key)) { + return false; + } + + return Element.Attribute(key) != null; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/MetadataIndexer.cs b/src/Microsoft.PackageManagement/Packaging/MetadataIndexer.cs new file mode 100644 index 000000000..8b343decc --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/MetadataIndexer.cs @@ -0,0 +1,83 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Xml.Linq; + using Utility.Collections; + using Utility.Extensions; + + public class MetadataIndexer { + private readonly Swidtag _swidtag; + + public MetadataIndexer(Swidtag swidtag) { + _swidtag = swidtag; + } + + /// + /// Returns the string value of a given attribute. + /// + /// + /// the string value of a given attribute. If the attribute does not exist, returns null. + [SuppressMessage("Microsoft.Design", "CA1043:UseIntegralOrStringArgumentForIndexers", Justification = "It's a collection of attributes.")] + public IEnumerable this[string key] { + get { + if (string.IsNullOrWhiteSpace(key)) { + return null; + } + return _swidtag.Meta.Where(each => { + try { + return (each.Attributes[key] != null); + } catch { + // happens when key is not a legal attribute name + } + return false; + }).Select(each => each.Attributes[key]).ReEnumerable(); + } + } + + /// + /// An enumeration of all the attributes in this element. + /// + public IEnumerable Keys { + get { + return _swidtag.Meta.SelectMany(each => each.Attributes.Keys).ReEnumerable(); + } + } + + /// + /// An enumeration of all the attribute values in this element. + /// + public IEnumerable Values { + get { + return _swidtag.Meta.SelectMany(each => each.Attributes.Values).ReEnumerable(); + } + } + + /// + /// The count of the attributes in this element + /// + public int Count { + get { + return Values.Count(); + } + } + + public override string ToString() { + return "{{{0}}}".format(Keys.Select(each => each.ToString()).JoinWithComma()); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/OptionCategory.cs b/src/Microsoft.PackageManagement/Packaging/OptionCategory.cs new file mode 100644 index 000000000..bcdbf6a98 --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/OptionCategory.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + public enum OptionCategory { + Package = 0, + Provider = 1, + Source = 2, + Install = 3 + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/OptionType.cs b/src/Microsoft.PackageManagement/Packaging/OptionType.cs new file mode 100644 index 000000000..fc165e89e --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/OptionType.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + public enum OptionType { + String = 0, + StringArray = 1, + Int = 2, + Switch = 3, + Folder = 4, + File = 5, + Path = 6, + Uri = 7, + SecureString = 8 + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/PackageSource.cs b/src/Microsoft.PackageManagement/Packaging/PackageSource.cs new file mode 100644 index 000000000..67b4172cc --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/PackageSource.cs @@ -0,0 +1,67 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Packaging { + using System; + using System.Collections.Generic; + using Implementation; + + /// + /// Represents a package source (repository) + /// + public class PackageSource { + internal Dictionary DetailsCollection = new Dictionary(); + public string Name {get; internal set;} + public string Location {get; internal set;} + + public string Source { + get { + return Name ?? Location; + } + } + + // todo: make this dictionary read only! (.net 4.0 doesn't have that!) + + public string ProviderName { + get { + return Provider.ProviderName; + } + } + + public PackageProvider Provider {get; internal set;} + public bool IsTrusted {get; internal set;} + public bool IsRegistered {get; internal set;} + public bool IsValidated {get; internal set;} + + public IDictionary Details { + get { + return DetailsCollection; + } + } + + public override bool Equals(Object obj) + { + PackageSource packageSource = obj as PackageSource; + if (packageSource == null) + return false; + else + return ((Name.Equals(packageSource.Name) && Location.Equals(packageSource.Location))); + } + + public override int GetHashCode() + { + return ((Name ?? String.Empty) + (Location ?? String.Empty)).GetHashCode(); + } + } +} diff --git a/src/Microsoft.PackageManagement/Packaging/Payload.cs b/src/Microsoft.PackageManagement/Packaging/Payload.cs new file mode 100644 index 000000000..04fb39afb --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/Payload.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System; + using System.Xml.Linq; + + /// + /// From the schema: + /// The items that may be installed on a device when the software is + /// installed. Note that Payload may be a superset of the items + /// installed and, depending on optimization systems for a device, + /// may or may not include every item that could be created or + /// executed on a device when software is installed. + /// In general, payload will be used to indicate the files that + /// may be installed with a software product and will often be a + /// superset of those files (i.e. if a particular optional + /// component is not installed, the files associated with that + /// component may be included in payload, but not installed on + /// the device). + /// + public class Payload : ResourceCollection { + internal Payload(XElement element) + : base(element) { + if (element.Name != Iso19770_2.Elements.Payload) { + throw new ArgumentException("Element is not of type 'Payload'", "element"); + } + } + + internal Payload() + : base(new XElement(Iso19770_2.Elements.Payload)) { + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/Process.cs b/src/Microsoft.PackageManagement/Packaging/Process.cs new file mode 100644 index 000000000..2a14b291d --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/Process.cs @@ -0,0 +1,67 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System; + using System.Xml.Linq; + + public class Process : Meta { + internal Process(XElement element) + : base(element) { + if (element.Name != Iso19770_2.Elements.Process) { + throw new ArgumentException("Element is not of type 'Process'", "element"); + } + } + + internal Process(string processName) + : base(new XElement(Iso19770_2.Elements.Process)) { + Name = processName; + } + + /// + /// From the swidtag schema: + /// The process name as it will be found in the devices process table. + /// + public string Name { + get { + return GetAttribute(Iso19770_2.Attributes.Name); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Name, value); + } + } + + /// + /// From the swidtag schema: + /// The file size in bytes of the file + /// + public int? Pid { + get { + var sz = GetAttribute(Iso19770_2.Attributes.Size); + if (sz != null) { + int result; + if (Int32.TryParse(sz, out result)) { + return result; + } + } + return null; + } + internal set { + if (value != null) { + AddAttribute(Iso19770_2.Attributes.Pid, value.ToString()); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/Resource.cs b/src/Microsoft.PackageManagement/Packaging/Resource.cs new file mode 100644 index 000000000..0bb11fff2 --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/Resource.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System; + using System.Diagnostics.CodeAnalysis; + using System.Xml.Linq; + + /// + /// Specifies an arbitrary or custom resource in a swidtag. + /// From the swidtag schema: + /// A element that can be used to provide arbitrary resource + /// information about an application installed on a device, or + /// evidence collected from a device. + /// + public class Resource : Meta { + internal Resource(XElement element) + : base(element) { + if (element.Name != Iso19770_2.Elements.Resource) { + throw new ArgumentException("Element is not of type 'Resource'", "element"); + } + } + + internal Resource(string type) + : base(new XElement(Iso19770_2.Elements.Resource)) { + Type = type; + } + + /// + /// Specifies the what type of a resource is being recorded. + /// From the swidtag schema: + /// The type of resource (ie, registrykey, port, rootUrl,etc..) + /// + [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethod", Justification = "ISO field name.")] + public string Type { + get { + return GetAttribute(Iso19770_2.Attributes.Type); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Type, value); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/ResourceCollection.cs b/src/Microsoft.PackageManagement/Packaging/ResourceCollection.cs new file mode 100644 index 000000000..db599ac4a --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/ResourceCollection.cs @@ -0,0 +1,94 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System.Collections.Generic; + using System.Linq; + using System.Xml.Linq; + using Utility.Collections; + + public class ResourceCollection : BaseElement { + internal ResourceCollection(XElement element) + : base(element) { + } + + /// + /// An enumeration of the child directories in the element. + /// + public IEnumerable Directories { + get { + return Element.Elements(Iso19770_2.Elements.Directory).Select(each => new Directory(each)).ReEnumerable(); + } + } + + /// + /// An enumeration of the Files contained in the element + /// + public IEnumerable Files { + get { + return Element.Elements(Iso19770_2.Elements.File).Select(each => new File(each)).ReEnumerable(); + } + } + + /// + /// An enumeration of the Process elements in the element. + /// + public IEnumerable Processes { + get { + return Element.Elements(Iso19770_2.Elements.Process).Select(each => new Process(each)).ReEnumerable(); + } + } + + /// + /// An enumeration of the resource elements in the element. + /// + public IEnumerable Resources { + get { + return Element.Elements(Iso19770_2.Elements.Resource).Select(each => new Resource(each)).ReEnumerable(); + } + } + + /// + /// Adds a child directory element. + /// + /// The newly created directory element + internal Directory AddDirectory(string directoryName) { + return AddElement(new Directory(directoryName)); + } + + /// + /// Adds a file element. + /// + /// The newly created file element + internal File AddFile(string filename) { + return AddElement(new File(filename)); + } + + /// + /// Adds a new Process element to the element + /// + /// the newly created process element + internal Process AddProcess(string name) { + return AddElement(new Process(name)); + } + + /// + /// Adds a new Resource element to the element + /// + /// the newly created Resource element + internal Resource AddResource(string type) { + return AddElement(new Resource(type)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/SoftwareIdentity.cs b/src/Microsoft.PackageManagement/Packaging/SoftwareIdentity.cs new file mode 100644 index 000000000..aa9e8c9b5 --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/SoftwareIdentity.cs @@ -0,0 +1,360 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Packaging { + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Xml.Linq; + using Implementation; + using Internal.Api; + using Internal.Packaging; + using Internal.Utility.Collections; + using Internal.Utility.Extensions; + + /// + /// This class represents a package (retrieved from Find-SoftwareIdentity or Get-SoftwareIdentity) + /// Will eventually also represent a swidtag. + /// + public class SoftwareIdentity : Swidtag { + public SoftwareIdentity(XDocument document) + : base(document) { + } + + public SoftwareIdentity() { + } + + public string FastPackageReference {get; set;} + internal PackageProvider Provider {get; set;} + + public string ProviderName { + get { + return Provider != null ? Provider.ProviderName : null; + } + } + + public string Source {get; internal set;} + public string Status {get; internal set;} + public string SearchKey {get; internal set;} + public string FullPath {get; internal set;} + public string PackageFilename {get; internal set;} + public bool FromTrustedSource {get; set;} + + public string Summary { + get { + + return Element.Elements(Iso19770_2.Elements.Meta).Select(each => { + var summary = each.GetAttribute(Iso19770_2.Attributes.Summary); + if (string.IsNullOrWhiteSpace(summary)) { + summary = each.GetAttribute(Iso19770_2.Attributes.Description); + } + return summary; + }).WhereNotNull().FirstOrDefault(); + } + internal set { + (Meta.FirstOrDefault() ?? AddMeta()).AddAttribute(Iso19770_2.Attributes.Summary, value); + } + } + + public IEnumerable SwidTags { + get { + // todo: in the not-too-distant future, the SoftwareIdentity will support a collection of swidtags in addition to 'itself' + // todo: at that time, we'll add the remaining logic to support collections of swidtags for a given SoftwareIdentity. + yield return this; + } + } + + public string CanonicalId { + get { + return CreateCanonicalId(ProviderName, Name, Version, Source); + } + } + + public MetadataIndexer Metadata { + get { + return new MetadataIndexer(this); + } + } + + internal string InstallationPath + { + get + { + // no payload or directories, just returns empty string + if (Payload == null || Payload.Directories == null) + { + return string.Empty; + } + + // find the installation directories based on the payload. + var installationDirectories = Payload.Directories + // Use the directory if it is key, if its location is not null or white space and its name is not null or whitespace + .Where(directory => directory.IsKey.HasValue + && (bool)directory.IsKey + && !string.IsNullOrWhiteSpace(directory.Location) + && !string.IsNullOrWhiteSpace(directory.Name)) + // combine location and name to get the directory + .Select(directory => System.IO.Path.Combine(directory.Location, directory.Name)); + + // if there is installation directories, concat them with comma + return installationDirectories.JoinWithComma(); + } + } + + internal static string CreateCanonicalId(string provider, string name, string version, string source) { + if (provider == null || name == null) { + return null; + } + if (string.IsNullOrWhiteSpace(version) && string.IsNullOrWhiteSpace(source)) { + return "{0}:{1}".format(CultureInfo.CurrentCulture.TextInfo.ToLower(provider), name); + } + if (string.IsNullOrWhiteSpace(source)) { + return "{0}:{1}/{2}".format(CultureInfo.CurrentCulture.TextInfo.ToLower(provider), name, version); + } + if (string.IsNullOrWhiteSpace(version)) { + "{0}:{1}#{2}".format(CultureInfo.CurrentCulture.TextInfo.ToLower(provider), name, source); + } + + return "{0}:{1}/{2}#{3}".format(CultureInfo.CurrentCulture.TextInfo.ToLower(provider), name, version, source); + } + + public void FetchPackageDetails(IHostApi api) { + Provider.GetPackageDetails(this, api); + } + + /// + /// Sets a SoftwareMetadata value + /// + /// + /// + internal void AddMetadataAttribute(string metaKey, string value) { + if (string.IsNullOrWhiteSpace(value)) { + // we don't store empty values. + return; + } + + var metaElements = Element.Elements(Iso19770_2.Elements.Meta).ReEnumerable(); + var currentValue = metaElements.Select(each => each.GetAttribute(metaKey)).WhereNotNull().FirstOrDefault(); + + // if the current value is already the value, then don't worry about it. + if (currentValue == value) { + return; + } + + if (metaElements.Any() && currentValue == null) { + // value has not been set (and we have at least one metadata element) + // let's set it in the first meta element. + Element.Elements(Iso19770_2.Elements.Meta).FirstOrDefault().AddAttribute(metaKey, value); + } else { + // add a new metadata object at the end and set the value in that element. + AddElement(new Meta()) + .AddAttribute(metaKey, value); + } + } + + protected override XElement FindElementWithUniqueId(string elementId) { + if (elementId == FastPackageReference) { + return Element; + } + return base.FindElementWithUniqueId(elementId); + } + + internal string AddMeta(string elementPath) { + var element = FindElementWithUniqueId(elementPath); + if (element != null) { + switch (element.Name.LocalName) { + case "SoftwareIdentity": + // adds a SoftwareMeta to the swidtag + return AddElement(new SoftwareMetadata()).ElementUniqueId; + + case "Entity": + // adds a Meta to the entity + return new Entity(element).AddMeta().ElementUniqueId; + } + } + return null; + } + + internal string AddLink(Uri referenceUri, string relationship, string mediaType, string ownership, string use, string appliesToMedia, string artifact) { + var link = AddLink(referenceUri, relationship); + + if (!string.IsNullOrWhiteSpace(mediaType)) { + link.MediaType = mediaType; + } + + if (!string.IsNullOrWhiteSpace(ownership)) { + link.Ownership = ownership; + } + + if (!string.IsNullOrWhiteSpace(use)) { + link.Use = use; + } + + if (!string.IsNullOrWhiteSpace(appliesToMedia)) { + link.Media = appliesToMedia; + } + + if (!string.IsNullOrWhiteSpace(artifact)) { + link.Artifact = artifact; + } + + return link.ElementUniqueId; + } + + internal string AddEntity(string name, string regid, string role, string thumbprint) { + var entity = AddEntity(name, regid, role); + + if (!string.IsNullOrWhiteSpace(thumbprint) && entity.Thumbprint == null) { + entity.Thumbprint = thumbprint; + } + + return entity.ElementUniqueId; + } + + public string AddMetadataValue(string elementPath, Uri @namespace, string name, string value) { + if (@namespace == null) { + return null; + } + var element = FindElementWithUniqueId(elementPath); + if (element != null) { + element.AddAttribute(XNamespace.Get(@namespace.ToString()) + name, value); + return PathToElement(element); + } + return null; + } + + internal string AddMetadataValue(string elementPath, string name, string value) { + var element = FindElementWithUniqueId(elementPath); + if (element == null || string.IsNullOrWhiteSpace(name)) { + return null; + } + + if (element == Element) { + // special case: + if (name.EqualsIgnoreCase("FromTrustedSource")) { + FromTrustedSource = (value ?? string.Empty).IsTrue(); + return FastPackageReference; + } + + // metadata values on the swidtag go to the first Meta + var meta = (Meta.FirstOrDefault() ?? AddMeta()); + meta.AddAttribute(name, value); + return meta.ElementUniqueId; + } + + if (element.Name == Iso19770_2.Elements.Entity) { + // metadata values on entities go to the first Meta in the entity + var entity = new Entity(element); + var meta = (entity.Meta.FirstOrDefault() ?? entity.AddMeta()); + meta.AddAttribute(name, value); + return meta.ElementUniqueId; + } + + // for non-namespaced metadata values, the target element must be one that inherits from Meta + if (IsMetaElement(element)) { + var meta = new Meta(element); + meta.AddAttribute(name, value); + return meta.ElementUniqueId; + } + + return null; + } + + internal string AddResource(string elementPath, string type) { + var element = FindElementWithUniqueId(elementPath); + if (element == null || string.IsNullOrWhiteSpace(type)) { + return null; + } + if (element.Name == Iso19770_2.Elements.Payload || element.Name == Iso19770_2.Elements.Evidence) { + return new ResourceCollection(element).AddResource(type).ElementUniqueId; + } + return null; + } + + internal string AddProcess(string elementPath, string processName, int pid) { + var element = FindElementWithUniqueId(elementPath); + if (element == null || string.IsNullOrWhiteSpace(processName)) { + return null; + } + if (element.Name == Iso19770_2.Elements.Payload || element.Name == Iso19770_2.Elements.Evidence) { + var process = new ResourceCollection(element).AddProcess(processName); + if (pid != 0) { + process.Pid = pid; + } + return process.ElementUniqueId; + } + return null; + } + + internal string AddFile(string elementPath, string fileName, string location, string root, bool isKey, long size, string version) { + var element = FindElementWithUniqueId(elementPath); + if (element == null || string.IsNullOrWhiteSpace(fileName)) { + return null; + } + if (element.Name == Iso19770_2.Elements.Payload || element.Name == Iso19770_2.Elements.Evidence || element.Name == Iso19770_2.Elements.Directory) { + var file = new ResourceCollection(element).AddFile(fileName); + if (!string.IsNullOrWhiteSpace(location)) { + file.Location = location; + } + if (!string.IsNullOrWhiteSpace(root)) { + file.Root = root; + } + if (isKey) { + file.IsKey = true; + } + if (size > 0) { + file.Size = size; + } + if (!string.IsNullOrWhiteSpace(version)) { + file.Version = version; + } + return file.ElementUniqueId; + } + return null; + } + + internal string AddDirectory(string elementPath, string directoryName, string location, string root, bool isKey) { + var element = FindElementWithUniqueId(elementPath); + if (element == null || string.IsNullOrWhiteSpace(directoryName)) { + return null; + } + if (element.Name == Iso19770_2.Elements.Payload || element.Name == Iso19770_2.Elements.Evidence || element.Name == Iso19770_2.Elements.Directory) { + var file = new ResourceCollection(element).AddDirectory(directoryName); + if (!string.IsNullOrWhiteSpace(location)) { + file.Location = location; + } + if (!string.IsNullOrWhiteSpace(root)) { + file.Root = root; + } + if (isKey) { + file.IsKey = true; + } + return file.ElementUniqueId; + } + return null; + } + + internal Evidence AddEvidence(DateTime date, string deviceId) { + var evidence = AddEvidence(); + evidence.Date = date; + evidence.DeviceId = deviceId; + return evidence; + } + + public string AddDependency(string providerName, string packageName, string version, string source, string appliesTo) { + return AddLink(new Uri(CreateCanonicalId(providerName, packageName, version, source)), Iso19770_2.Relationship.Requires, null, null, null, appliesTo, null); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/SoftwareIdentityNameVersionComparer.cs b/src/Microsoft.PackageManagement/Packaging/SoftwareIdentityNameVersionComparer.cs new file mode 100644 index 000000000..95893fa40 --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/SoftwareIdentityNameVersionComparer.cs @@ -0,0 +1,58 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using Microsoft.PackageManagement.Internal.Utility.Extensions; +using Microsoft.PackageManagement.Packaging; + +namespace Microsoft.PackageManagement.Internal.Packaging +{ + /// + /// 2 Swids will be equal if their names and versions are the same + /// + public class SoftwareIdentityNameVersionComparer : IEqualityComparer + { + private static SoftwareIdentityVersionComparer VersionComparer = new SoftwareIdentityVersionComparer(); + + public bool Equals(SoftwareIdentity swidOne, SoftwareIdentity swidTwo) + { + // True if both are null + if (swidOne == null && swidTwo == null) + { + return true; + } + + // False if 1 is null and the other is not + if (swidOne == null || swidTwo == null) + { + return false; + } + + // true if name is same and version is same + return String.Equals(swidOne.Name, swidTwo.Name, StringComparison.OrdinalIgnoreCase) && (VersionComparer.Compare(swidOne, swidTwo) == 0); + } + + public int GetHashCode(SoftwareIdentity obj) + { + if (obj == null) + { + return 0; + } + + return (String.IsNullOrWhiteSpace(obj.Name) ? String.Empty : obj.Name).GetHashCode() * 31 + + (String.IsNullOrWhiteSpace(obj.Version) ? String.Empty : obj.Version).GetHashCode(); + } + } +} diff --git a/src/Microsoft.PackageManagement/Packaging/SoftwareIdentityVersionComparer.cs b/src/Microsoft.PackageManagement/Packaging/SoftwareIdentityVersionComparer.cs new file mode 100644 index 000000000..eae47feef --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/SoftwareIdentityVersionComparer.cs @@ -0,0 +1,192 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System; + using System.Collections.Generic; + using PackageManagement.Packaging; + using Utility.Extensions; + + public class SoftwareIdentityVersionComparer : IComparer { + public static SoftwareIdentityVersionComparer Instance = new SoftwareIdentityVersionComparer(); + + public int Compare(SoftwareIdentity x, SoftwareIdentity y) { + if (x == null || y == null) { + // can't compare vs null. + return 0; + } + var xVersionScheme = x.VersionScheme ?? string.Empty; + var yVersionScheme = y.VersionScheme ?? string.Empty; + + if (!x.VersionScheme.EqualsIgnoreCase(yVersionScheme)) { + // can't compare versions between different version schemes + return 0; + } + + return CompareVersions(xVersionScheme, x.Version, y.Version); + } + + public static int CompareVersions(string versionScheme, string xVersion, string yVersion) { + xVersion = (xVersion ?? string.Empty).Trim(); + yVersion = (yVersion ?? string.Empty).Trim(); + + // regardless of type, if the strings are equal, it's the same version. + // if we have two nulls or blanks, this works the same + if (xVersion.Equals(yVersion)) { + return 0; + } + + if (xVersion.IsNullOrEmpty()) { + // yVersion has a value and is always therefore greater than not a value + return -1; + } + + if (yVersion.IsNullOrEmpty()) { + // xVersion has a value and is always therefore greater than not a value + return 1; + } + + switch ((versionScheme ?? "unknown").ToLowerInvariant()) { + case Iso19770_2.VersionScheme.Alphanumeric: + // string sort + return String.Compare(xVersion, yVersion, StringComparison.Ordinal); + + case Iso19770_2.VersionScheme.Decimal: + double xDouble; + double yDouble; + if (double.TryParse(xVersion, out xDouble) && double.TryParse(yVersion, out yDouble)) { + return xDouble.CompareTo(yDouble); + } + return 0; + + case Iso19770_2.VersionScheme.MultipartNumeric: + return CompareMultipartNumeric(xVersion, yVersion); + + case Iso19770_2.VersionScheme.MultipartNumericPlusSuffix: + return CompareMultipartNumericSuffix(xVersion, yVersion); + + case Iso19770_2.VersionScheme.SemVer: + return CompareSemVer(xVersion, yVersion); + + case Iso19770_2.VersionScheme.Unknown: + return GuessComparison(xVersion, yVersion); + + default: + return GuessComparison(xVersion, yVersion); + } + } + + private static int GuessComparison(string xVersion, string yVersion) { + // if either one starts with a dot, prepend a zero + if (xVersion[0] == '.') { + xVersion = "0" + xVersion; + } + if (yVersion[0] == '.') { + yVersion = "0" + yVersion; + } + + // if they both start with numbers, we're going to treat them as multipart numeric with suffix. + if (xVersion.IndexWhere(ch => ch >= '0' && ch <= '9') == 0 && yVersion.IndexWhere(ch => ch >= '0' && ch <= '9') == 0) { + return CompareMultipartNumericSuffix(xVersion, yVersion); + } + + // otherwise, we really don't know + return 0; + } + + private static int CompareMultipartNumeric(string xVersion, string yVersion) { + var xs = xVersion.Split('.'); + var ys = yVersion.Split('.'); + var len = Math.Max(xs.Length, ys.Length); + for (var i = 0; i < len; i++) { + ulong xLong; + ulong yLong; + + if (ulong.TryParse(xs.Length > i ? xs[i] : "0", out xLong) && ulong.TryParse(ys.Length > i ? ys[i] : "0", out yLong)) { + var compare = xLong.CompareTo(yLong); + if (compare != 0) { + return compare; + } + continue; + } + return 0; + } + return 0; + } + + private static int CompareMultipartNumericSuffix(string xVersion, string yVersion) { + var xPos = IndexOfNonNumericWithDots(xVersion); + var yPos = IndexOfNonNumericWithDots(yVersion); + var xMulti = xPos == -1 ? xVersion : xVersion.Substring(0, xPos); + var yMulti = yPos == -1 ? yVersion : yVersion.Substring(0, yPos); + var compare = CompareMultipartNumeric(xMulti, yMulti); + if (compare != 0) { + return compare; + } + + if (xPos == -1 && yPos == -1) { + // no suffixes? + return 0; + } + + if (xPos == -1) { + // x has no suffix, y does + // y is later. + return -1; + } + + if (yPos == -1) { + // x has suffix, y doesn't + // x is later. + return 1; + } + + return String.Compare(xVersion.Substring(xPos), yVersion.Substring(yPos), StringComparison.Ordinal); + } + + private static int CompareSemVer(string xVersion, string yVersion) { + var xPos = IndexOfNonNumericWithDots(xVersion); + var yPos = IndexOfNonNumericWithDots(yVersion); + var xMulti = xPos == -1 ? xVersion : xVersion.Substring(0, xPos); + var yMulti = yPos == -1 ? yVersion : yVersion.Substring(0, yPos); + var compare = CompareMultipartNumeric(xMulti, yMulti); + if (compare != 0) { + return compare; + } + + if (xPos == -1 && yPos == -1) { + // no suffixes? + return 0; + } + + if (xPos == -1) { + // x has no suffix, y does + // x is later. + return 1; + } + + if (yPos == -1) { + // x has suffix, y doesn't + // y is later. + return -1; + } + + return String.Compare(xVersion.Substring(xPos), yVersion.Substring(yPos), StringComparison.Ordinal); + } + + private static int IndexOfNonNumericWithDots(string version) { + return string.IsNullOrEmpty(version) ? -1 : version.IndexWhere(ch => (ch < '0' || ch > '9') && ch != '.'); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/SoftwareMetadata.cs b/src/Microsoft.PackageManagement/Packaging/SoftwareMetadata.cs new file mode 100644 index 000000000..e04bce3e2 --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/SoftwareMetadata.cs @@ -0,0 +1,163 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System; + using System.Xml.Linq; + using Utility.Extensions; + + public class SoftwareMetadata : Meta { + internal SoftwareMetadata(XElement element) : base(element) { + if (element.Name != Iso19770_2.Elements.Meta) { + throw new ArgumentException("Element is not of type 'SoftwareMetadata'", "element"); + } + } + + internal SoftwareMetadata() + : base(new XElement(Iso19770_2.Elements.Meta)) { + } + + public string ActivationStatus { + get { + return GetAttribute(Iso19770_2.Attributes.ActivationStatus); + } + internal set { + AddAttribute(Iso19770_2.Attributes.ActivationStatus, value); + } + } + + public string ChannelType { + get { + return GetAttribute(Iso19770_2.Attributes.ChannelType); + } + internal set { + AddAttribute(Iso19770_2.Attributes.ChannelType, value); + } + } + + public string Description { + get { + return GetAttribute(Iso19770_2.Attributes.Description); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Description, value); + } + } + + public string ColloquialVersion { + get { + return GetAttribute(Iso19770_2.Attributes.ColloquialVersion); + } + internal set { + AddAttribute(Iso19770_2.Attributes.ColloquialVersion, value); + } + } + + public string Edition { + get { + return GetAttribute(Iso19770_2.Attributes.Edition); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Edition, value); + } + } + + public string EntitlementKey { + get { + return GetAttribute(Iso19770_2.Attributes.EntitlementKey); + } + internal set { + AddAttribute(Iso19770_2.Attributes.EntitlementKey, value); + } + } + + public string Generator { + get { + return GetAttribute(Iso19770_2.Attributes.Generator); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Generator, value); + } + } + + public string PersistentId { + get { + return GetAttribute(Iso19770_2.Attributes.PersistentId); + } + internal set { + AddAttribute(Iso19770_2.Attributes.PersistentId, value); + } + } + + public string Product { + get { + return GetAttribute(Iso19770_2.Attributes.Product); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Product, value); + } + } + + public string ProductFamily { + get { + return GetAttribute(Iso19770_2.Attributes.ProductFamily); + } + internal set { + AddAttribute(Iso19770_2.Attributes.ProductFamily, value); + } + } + + public string Revision { + get { + return GetAttribute(Iso19770_2.Attributes.Revision); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Revision, value); + } + } + + public string UnspscCode { + get { + return GetAttribute(Iso19770_2.Attributes.UnspscCode); + } + internal set { + AddAttribute(Iso19770_2.Attributes.UnspscCode, value); + } + } + + public string UnspscVersion { + get { + return GetAttribute(Iso19770_2.Attributes.UnspscVersion); + } + internal set { + AddAttribute(Iso19770_2.Attributes.UnspscVersion, value); + } + } + + public bool? EntitlementDataRequired { + get { + return GetAttribute(Iso19770_2.Attributes.EntitlementDataRequired).IsTruePreserveNull(); + } + internal set { + if (value != null) { + AddAttribute(Iso19770_2.Attributes.EntitlementDataRequired, value.ToString()); + } + } + } + + public override string ToString() { + return Attributes.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Packaging/Swidtag.cs b/src/Microsoft.PackageManagement/Packaging/Swidtag.cs new file mode 100644 index 000000000..a86ca7d4f --- /dev/null +++ b/src/Microsoft.PackageManagement/Packaging/Swidtag.cs @@ -0,0 +1,306 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Packaging { + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Xml; + using System.Xml.Linq; + using Utility.Collections; + using Utility.Extensions; + + public class Swidtag : BaseElement { + internal XDocument _swidTag; + + public Swidtag(XDocument document) + : base(document) { + _swidTag = document; + ChangeCurrentNameSpace(); + } + + public Swidtag() + : this(new XDocument( + new XDeclaration("1.0", "UTF-8", "yes"), + new XElement(Iso19770_2.Elements.SoftwareIdentity))) { + } + + public Swidtag(XElement xmlDocument) + : this(new XDocument( + new XDeclaration("1.0", "UTF-8", "yes"), + xmlDocument)) { + } + + internal void SetSwidTag(XDocument swidTag) + { + _swidTag = swidTag; + ChangeCurrentNameSpace(); + Element = _swidTag.Root; + } + + /// + /// This method is called to change namespace from Iso19770_2_Current to Iso19770_2 + /// + private void ChangeCurrentNameSpace() + { + if (_swidTag.Root.GetDefaultNamespace() == Iso19770_2.Namespace.Iso19770_2_Current) + { + // change namespace of the document + // this is based on https://msdn.microsoft.com/en-us/library/bb943914.aspx + + foreach (XElement el in _swidTag.Root.DescendantsAndSelf()) + { + + el.Name = Iso19770_2.Namespace.Iso19770_2.GetName(el.Name.LocalName); + + List atList = el.Attributes().ToList(); + + el.Attributes().Remove(); + + foreach (XAttribute at in atList) + { + el.Add(new XAttribute(Iso19770_2.Namespace.Iso19770_2.GetName(at.Name.LocalName), at.Value)); + } + } + } + } + + public string SwidTagText { + get { + var stringBuilder = new StringBuilder(); + + var settings = new XmlWriterSettings { + OmitXmlDeclaration = false, + Indent = true, + NewLineOnAttributes = true, + NamespaceHandling = NamespaceHandling.OmitDuplicates + }; + + using (var xmlWriter = XmlWriter.Create(stringBuilder, settings)) { + _swidTag.Save(xmlWriter); + } + + return stringBuilder.ToString(); + } + } + + public static bool IsSwidtag(XElement xmlDocument) { + return xmlDocument.Name == Iso19770_2.Elements.SoftwareIdentity || xmlDocument.Name == Iso19770_2.Elements.SoftwareIdentityCurrent; + } + + public bool IsApplicable(Hashtable environment) { + return MediaQuery.IsApplicable(AppliesToMedia, environment); + } + + public IEnumerable Dependencies + { + get + { + return Links.Where(each => Iso19770_2.Relationship.Requires == each.Relationship).Select(each => each.HRef).WhereNotNull().Select(each => Uri.UnescapeDataString(each.ToString())).ReEnumerable(); + } + } + + #region Attributes + + public bool? IsCorpus { + get { + return GetAttribute(Iso19770_2.Attributes.Corpus).IsTruePreserveNull(); + } + internal set { + if (value != null) { + AddAttribute(Iso19770_2.Attributes.Corpus, value.ToString()); + } + } + } + + public virtual string Name { + get { + return GetAttribute(Iso19770_2.Attributes.Name); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Name, value); + } + } + + public string Version { + get { + return GetAttribute(Iso19770_2.Attributes.Version); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Version, value); + } + } + + public string VersionScheme { + get { + return GetAttribute(Iso19770_2.Attributes.VersionScheme); + } + internal set { + AddAttribute(Iso19770_2.Attributes.VersionScheme, value); + } + } + + public string TagVersion { + get { + return GetAttribute(Iso19770_2.Attributes.TagVersion); + } + internal set { + AddAttribute(Iso19770_2.Attributes.TagVersion, value); + } + } + + public string TagId { + get { + return GetAttribute(Iso19770_2.Attributes.TagId); + } + internal set { + AddAttribute(Iso19770_2.Attributes.TagId, value); + } + } + + public bool? IsPatch { + get { + return GetAttribute(Iso19770_2.Attributes.Patch).IsTruePreserveNull(); + } + internal set { + if (value != null) { + AddAttribute(Iso19770_2.Attributes.Patch, value.ToString()); + } + } + } + + public bool? IsSupplemental { + get { + return GetAttribute(Iso19770_2.Attributes.Supplemental).IsTruePreserveNull(); + } + internal set { + if (value != null) { + AddAttribute(Iso19770_2.Attributes.Supplemental, value.ToString()); + } + } + } + + public string AppliesToMedia { + get { + return GetAttribute(Iso19770_2.Attributes.Media); + } + internal set { + AddAttribute(Iso19770_2.Attributes.Media, value); + } + } + + #endregion + + #region Elements + + public IEnumerable Meta { + get { + return Element.Elements(Iso19770_2.Elements.Meta).Select(each => new SoftwareMetadata(each)).ReEnumerable(); + } + } + + internal SoftwareMetadata AddMeta() { + return AddElement(new SoftwareMetadata()); + } + + public IEnumerable Links { + get { + return Element.Elements(Iso19770_2.Elements.Link).Select(each => new Link(each)).ReEnumerable(); + } + } + + internal Link AddLink(Uri referenceUri, string relationship) { + return AddElement(new Link(referenceUri, relationship)); + } + + public IEnumerable Entities { + get { + return Element.Elements(Iso19770_2.Elements.Entity).Select(each => new Entity(each)).ReEnumerable(); + } + } + + internal Entity AddEntity(string name, string regId, string role) { + return AddElement(new Entity(name, regId, role)); + } + + /// + /// A ResourceCollection for the 'Payload' of the SwidTag. + /// This value is null if the swidtag does not contain a Payload element. + /// from swidtag XSD: + /// The items that may be installed on a device when the software is + /// installed. Note that Payload may be a superset of the items + /// installed and, depending on optimization systems for a device, + /// may or may not include every item that could be created or + /// executed on a device when software is installed. + /// In general, payload will be used to indicate the files that + /// may be installed with a software product and will often be a + /// superset of those files (i.e. if a particular optional + /// component is not installed, the files associated with that + /// component may be included in payload, but not installed on + /// the device). + /// + public Payload Payload { + get { + return Element.Elements(Iso19770_2.Elements.Payload).Select(each => new Payload(each)).FirstOrDefault(); + } + } + + /// + /// Adds a Payload resource collection element. + /// + /// The ResourceCollection added. If the Payload already exists, returns the current Payload. + internal Payload AddPayload() { + // should we just detect and add the evidence element when a provider is adding items to the evidence + // instead of requiring someone to explicity add the element? + if (Element.Elements(Iso19770_2.Elements.Payload).Any()) { + return Payload; + } + return AddElement(new Payload()); + } + + /// + /// An Evidence element representing the discovered for a swidtag. + /// This value is null if the swidtag does not contain an Evidence element + /// from swidtag XSD: + /// The element is used to provide results from a scan of a system + /// where software that does not have a SWID tag is discovered. + /// This information is not provided by the software creator, and + /// is instead created when a system is being scanned and the + /// evidence for why software is believed to be installed on the + /// device is provided in the Evidence element. + /// + public Evidence Evidence { + get { + return Element.Elements(Iso19770_2.Elements.Evidence).Select(each => new Evidence(each)).FirstOrDefault(); + } + } + + /// + /// Adds an Evidence element. + /// + /// The added Evidence element. If the Evidence element already exists, returns the current element. + internal Evidence AddEvidence() { + // should we just detect and add the evidence element when a provider is adding items to the evidence + // instead of requiring someone to explicity add the element? + if (Element.Elements(Iso19770_2.Elements.Evidence).Any()) { + return Evidence; + } + return AddElement(new Evidence()); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Properties/AssemblyInfo.cs b/src/Microsoft.PackageManagement/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..e72eb47bb --- /dev/null +++ b/src/Microsoft.PackageManagement/Properties/AssemblyInfo.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. + +[assembly: AssemblyTitle("Microsoft.PackageManagement")] +[assembly: AssemblyDescription("PackageManagement Core")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("PackageManagement")] +[assembly: AssemblyCopyright("Copyright Microsoft © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Normally, we wouldn't actaully permit access to the internals of the API +// but we're sharing code with the other tightly-coupled providers +// Third-party providers shouldn't use (or want) to be tightly-coupled + +[assembly: InternalsVisibleTo("Microsoft.PowerShell.PackageManagement")] +[assembly: InternalsVisibleTo("Microsoft.PackageManagement.Test")] +[assembly: InternalsVisibleTo("Microsoft.PackageManagement.MetaProvider.PowerShell")] +[assembly: InternalsVisibleTo("Microsoft.PackageManagement.CoreProviders")] +[assembly: InternalsVisibleTo("Microsoft.PackageManagement.ArchiverProviders")] +[assembly: InternalsVisibleTo("Microsoft.PackageManagement.MsiProvider")] +[assembly: InternalsVisibleTo("Microsoft.PackageManagement.MsuProvider")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM + +[assembly: Guid("2335ef65-8af7-4923-8d8b-a9e6943e4ff9")] diff --git a/src/Microsoft.PackageManagement/ReadMe.txt b/src/Microsoft.PackageManagement/ReadMe.txt new file mode 100644 index 000000000..19770f127 --- /dev/null +++ b/src/Microsoft.PackageManagement/ReadMe.txt @@ -0,0 +1,45 @@ +OneGet Experimental Build. +--------------------------- + +Unpack into a folder somewhere. + +DO NOT OVERWRITE A ONEGET MODULE INSTALLED WITH WMF 5. + +Run the RunToUnblock.cmd script to unblock the files. +This also sets the executionpolicy to unrestricted. + +Open a powershell session and import the module using the path: + + PS > ipmo c:\path\to\module\oneget.psd1 + + +Release Notes +--------------------------- +This build has the return of the Chocolatey Provider (Yay!) +It's still experimental, but is getting better every day. + +The NuGet provider is hidden by default, you can access it using + -provider NuGet +on most commands. + +There is also an MSI provider, which will install and uninstall apps using +MSI files. + +The ARP provider can show applications listed in the ADD/REMOVE programs. Does +not yet uninstall. + +It's now built using .NET 4.0 -- this may run on Windows 7 +and Server 2008 R2. on WMF 3. Please test it :D + + +Changelog +-------------- +==LOG== + + +Not Functional Yet: +--------------------------- + + The MSU provider is not yet complete + The VSIX provider is not yet complete + The SWIDTAG provider is not yet complete diff --git a/src/Microsoft.PackageManagement/Utility/Async/AsyncAction.cs b/src/Microsoft.PackageManagement/Utility/Async/AsyncAction.cs new file mode 100644 index 000000000..06efb9bb3 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Async/AsyncAction.cs @@ -0,0 +1,303 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Async { + using System; + using System.Diagnostics; + using System.Threading; + + public abstract class AsyncAction : IAsyncAction { + private static readonly TimeSpan DefaultCallTimeout = TimeSpan.FromMinutes(120); + private static readonly TimeSpan DefaultResponsiveness = TimeSpan.FromMinutes(120); + private ActionState _actionState; + protected DateTime _callStart = DateTime.Now; + private DisposalState _disposalState; + protected Thread _invocationThread; + private DateTime _lastActivity = DateTime.Now; + private TimeSpan _responsiveness = DefaultResponsiveness; + private TimeSpan _timeout = DefaultCallTimeout; + private Timer _timer; + protected readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private readonly ManualResetEventSlim _completed = new ManualResetEventSlim(false); + private readonly object _lock = new Object(); + + protected AsyncAction() { + _timer = new Timer(Signalled, this, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); + } + + private TimeSpan TimeLeft { + get { + if (_actionState >= ActionState.Aborting) { + return TimeSpan.FromMilliseconds(System.Threading.Timeout.Infinite); + } + + if (_actionState >= ActionState.Cancelling) { + var timeToAbort = _responsiveness.Subtract(DateTime.Now.Subtract(_lastActivity)); + return timeToAbort < TimeSpan.Zero ? TimeSpan.Zero : timeToAbort; + } + + var timeToRespond = _responsiveness.Subtract(DateTime.Now.Subtract(_lastActivity)); + if (timeToRespond < TimeSpan.Zero) { + timeToRespond = TimeSpan.Zero; + } + + var timeToTimeout = _timeout.Subtract(DateTime.Now.Subtract(_callStart)); + if (timeToTimeout < TimeSpan.Zero) { + timeToTimeout = TimeSpan.Zero; + } + return timeToRespond < timeToTimeout ? timeToRespond : timeToTimeout; + } + } + + public event Action OnComplete; + public event Action OnCancel; + public event Action OnAbort; + + public virtual void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + public virtual void Cancel() { + // if it's already done, this is a no-op + lock (_lock) { + if (_actionState >= ActionState.Cancelling) { + return; + } + _actionState = ActionState.Cancelling; + } + + // activate the cancellation token for those who are watching for that. + _cancellationTokenSource.Cancel(); + + // actively tell anyone who is listening that we're trying to cancel this. + if (OnCancel != null) { + OnCancel(); + } + lock (_lock) { + if (_actionState < ActionState.Canceled) { + _actionState = ActionState.Canceled; + } + } + } + + public virtual void Abort() { + // make sure we're at least cancelled first! + Cancel(); + + // if it's already done, this is a no-op + lock (_lock) { + if (_actionState >= ActionState.Aborting) { + return; + } + _actionState = ActionState.Aborting; + } + + // we have no need left for this. + DisposeTimer(); + + // notify any listeners that we're about to kill this. + if (OnAbort != null) { + OnAbort(); + } + + lock (_lock) { + if (_actionState < ActionState.Aborted) { + _actionState = ActionState.Aborted; + } + } + + // and make sure this is marked as complete + Complete(); + } + + public WaitHandle CompleteEvent { + get { + return _completed.WaitHandle; + } + } + + public TimeSpan Timeout { + get { + return _timeout; + } + set { + _timeout = value; + ResetTimer(); + } + } + + public TimeSpan Responsiveness { + get { + return _responsiveness; + } + set { + _responsiveness = value; + ResetTimer(); + } + } + + public virtual bool IsCanceled { + get { + return _cancellationTokenSource.IsCancellationRequested; + } + } + + public bool IsAborted { + get { + return _actionState == ActionState.Aborting || _actionState == ActionState.Aborted; + } + } + + public bool IsCompleted { + get { + return _completed.IsSet; + } + } + + public virtual void Dispose(bool disposing) { + lock (_lock) { + // make sure this kind of thing doesn't happen twice. + if (_disposalState > DisposalState.None) { + return; + } + _disposalState = DisposalState.Disposing; + } + + if (disposing) { + // Ensure we're cancelled first. + Cancel(); + + if (_actionState < ActionState.Aborting) { + // if we're not already in the process of aborting, we'll kick that off in a few seconds. + _timer.Change(5000, System.Threading.Timeout.Infinite); + } else { + // stop timer activity + DisposeTimer(); + } + + // for all intents, we're completed...even if the abort will run after this. + _completed.Set(); + _disposalState = DisposalState.Disposed; + if (_actionState >= ActionState.Completed) { + _completed.Dispose(); + _cancellationTokenSource.Dispose(); + } + } + } + + private void DisposeTimer() { + lock (_lock) { + if (_timer != null) { + _timer.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); + _timer.Dispose(); + _timer = null; + } + } + } + + protected virtual void Complete() { + lock (_lock) { + if (_actionState == ActionState.Completed) { + return; + } + _actionState = ActionState.Completed; + } + + DisposeTimer(); + + if (OnComplete != null) { + OnComplete(); + } + _completed.Set(); + } + + public virtual void WarnBeforeResponsivenessCancellation() { + } + + public virtual void WarnBeforeTimeoutCancellation() { + } + + private void Signalled(object obj) { + // if we're in the debugger, don't ever automatically cancel or abort. + + if (Debugger.IsAttached) { + return; + } + + if (_actionState > ActionState.Aborting) { + // we don't have anything to do here. + return; + } + + if (_actionState < ActionState.Cancelling) { + if (_responsiveness.Subtract(DateTime.Now.Subtract(_lastActivity)) < TimeSpan.Zero) { + // we have to cancel because the provider isn't responsive enough + WarnBeforeResponsivenessCancellation(); + } else { + // we have to cancel because the provider didn't complete the call in the time required. + WarnBeforeTimeoutCancellation(); + } + + // disabling actual cancellation until providers can deal correctly. + // Cancel(); + return; + } + + if (_actionState == ActionState.Cancelling) { + // we were in a cancelled state when we noticed the timer hit zero. + return; + } + + if (_actionState == ActionState.Canceled) { + // we were in a cancelled state when we noticed the timer hit zero. + Abort(); + } + } + + protected void Activity() { + _lastActivity = DateTime.Now; + ResetTimer(); + } + + protected void StartCall() { + _callStart = DateTime.Now; + ResetTimer(); + } + + private void ResetTimer() { + lock (_lock) { + if (_actionState <= ActionState.Canceled && _timer != null) { + _timer.Change(TimeLeft, TimeSpan.FromMilliseconds(System.Threading.Timeout.Infinite)); + } + } + } + + private enum ActionState { + None, + Called, + Cancelling, + Canceled, + Aborting, + Aborted, + Completed, + } + + private enum DisposalState { + None, + Disposing, + Disposed + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Async/AsyncActionExtensions.cs b/src/Microsoft.PackageManagement/Utility/Async/AsyncActionExtensions.cs new file mode 100644 index 000000000..a3e5e17fc --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Async/AsyncActionExtensions.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Async { + using System; + using System.Threading; + using System.Threading.Tasks; + + internal static class AsyncActionExtensions { + public static T Timeout(this T asyncAction, int seconds) where T : IAsyncAction { + asyncAction.Timeout = TimeSpan.FromSeconds(seconds); + return asyncAction; + } + + public static T Responsiveness(this T asyncAction, int seconds) where T : IAsyncAction { + asyncAction.Responsiveness = TimeSpan.FromSeconds(seconds); + return asyncAction; + } + + public static T CancelWhen(this T asyncAction, CancellationToken cancellationToken) where T : IAsyncAction { + cancellationToken.Register(asyncAction.Cancel); + return asyncAction; + } + + public static T AbortWhen(this T asyncAction, CancellationToken cancellationToken) where T : IAsyncAction { + cancellationToken.Register(asyncAction.Abort); + return asyncAction; + } + + public static T Wait(this T asyncAction) where T : IAsyncAction { + asyncAction.CompleteEvent.WaitOne(); + return asyncAction; + } + + public static T Wait(this T asyncAction, int milliseconds) where T : IAsyncAction { + asyncAction.CompleteEvent.WaitOne(milliseconds); + return asyncAction; + } + + public static T Wait(this T asyncAction, TimeSpan timespan) where T : IAsyncAction { + asyncAction.CompleteEvent.WaitOne(timespan); + return asyncAction; + } + + public static T OnCompletion(this T asyncAction, Action onCompleteAction) where T : class, IAsyncAction { + asyncAction.OnComplete += onCompleteAction; + return asyncAction; + } + + public static T OnCancellation(this T asyncAction, Action onCancelAction) where T : class, IAsyncAction { + asyncAction.OnCancel += onCancelAction; + return asyncAction; + } + + public static T OnAborted(this T asyncAction, Action onAbortAction) where T : class, IAsyncAction { + asyncAction.OnAbort += onAbortAction; + return asyncAction; + } + + public static Task AsTask(this IAsyncValue asyncValue) { + var tcs = new TaskCompletionSource(TaskCreationOptions.None); + asyncValue.OnCancel += tcs.SetCanceled; + asyncValue.OnAbort += () => tcs.SetException(new Exception("aborted")); + asyncValue.OnComplete += () => tcs.SetResult(asyncValue.Value); + return tcs.Task; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Async/EmptyAsyncEnumerable.cs b/src/Microsoft.PackageManagement/Utility/Async/EmptyAsyncEnumerable.cs new file mode 100644 index 000000000..3155d4a67 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Async/EmptyAsyncEnumerable.cs @@ -0,0 +1,121 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Async { + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + + public class EmptyAsyncEnumerable : IAsyncEnumerable { + private static readonly ManualResetEventSlim _completed = new ManualResetEventSlim(true); + + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void Cancel() { + // no-effect + if (OnCancel != null) { + OnCancel(); + OnCancel = null; + } + } + + public void Abort() { + if (OnAbort != null) { + OnAbort(); + OnAbort = null; + } + } + + public WaitHandle CompleteEvent { + get { + return _completed.WaitHandle; + } + } + + public TimeSpan Timeout {get; set;} + public TimeSpan Responsiveness {get; set;} + + public bool IsCanceled { + get { + return false; + } + } + + public bool IsAborted { + get { + return false; + } + } + + public bool IsCompleted { + get { + return true; + } + } + + public event Action OnComplete; + public event Action OnCancel; + public event Action OnAbort; + + public IEnumerator GetEnumerator() { + return Enumerable.Empty().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + + public IEnumerable GetConsumingEnumerable() { + return Enumerable.Empty(); + } + + public IEnumerable GetBlockingEnumerable() { + return Enumerable.Empty(); + } + + public bool IsConsumed { + get { + return true; + } + } + + public bool HasData { + get { + return false; + } + } + + public WaitHandle Ready { + get { + // the empty collection is always 'ready' + return _completed.WaitHandle; + } + } + + public virtual void Dispose(bool disposing) { + if (disposing) { + if (OnComplete != null) { + OnComplete(); + OnComplete = null; + } + _completed.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Async/IAsyncAction.cs b/src/Microsoft.PackageManagement/Utility/Async/IAsyncAction.cs new file mode 100644 index 000000000..873cfbba1 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Async/IAsyncAction.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Async { + using System; + using System.Threading; + + public interface IAsyncAction : IDisposable { + WaitHandle CompleteEvent {get;} + TimeSpan Timeout {get; set;} + TimeSpan Responsiveness {get; set;} + bool IsCanceled {get;} + bool IsAborted {get;} + bool IsCompleted {get;} + void Cancel(); + void Abort(); + event Action OnComplete; + event Action OnCancel; + event Action OnAbort; + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Async/IAsyncEnumerable.cs b/src/Microsoft.PackageManagement/Utility/Async/IAsyncEnumerable.cs new file mode 100644 index 000000000..04e40d738 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Async/IAsyncEnumerable.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Async { + using System.Collections.Generic; + using System.Threading; + + public interface IAsyncEnumerable : IAsyncAction, IEnumerable { + bool IsConsumed {get;} + bool HasData {get;} + WaitHandle Ready {get;} + IEnumerable GetConsumingEnumerable(); + IEnumerable GetBlockingEnumerable(); + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Async/IAsyncValue.cs b/src/Microsoft.PackageManagement/Utility/Async/IAsyncValue.cs new file mode 100644 index 000000000..d96e7f835 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Async/IAsyncValue.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Async { + public interface IAsyncValue : IAsyncAction { + T Value {get;} + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Collections/BlockingCollection.cs b/src/Microsoft.PackageManagement/Utility/Collections/BlockingCollection.cs new file mode 100644 index 000000000..df160aa6f --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Collections/BlockingCollection.cs @@ -0,0 +1,132 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Collections { + using System.Collections; + using System.Collections.Generic; + using System.Threading; + + internal class BlockingCollection : System.Collections.Concurrent.BlockingCollection, IEnumerable { + private MutableEnumerable _blockingEnumerable; + private readonly ManualResetEventSlim _activity = new ManualResetEventSlim(false); + private readonly object _lock = new object(); + + public WaitHandle Ready { + get { + return _activity.WaitHandle; + } + } + + public bool HasData { + get { + return Count > 0; + } + } + + public IEnumerator GetEnumerator() { + // make sure that iterating on this as enumerable is blocking. + return this.GetBlockingEnumerable().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + + protected override void Dispose(bool isDisposing) { + if (isDisposing) { + _blockingEnumerable = null; + } + } + + private void SetActivity() { + // setting _activity to true allows consumers to Wait for data to show up or for the producer to complete. + if (HasData || IsCompleted) { + _activity.Set(); + } else { + _activity.Reset(); + } + } + + public new void Add(T item) { + lock (_lock) { + if (!IsAddingCompleted) { + base.Add(item); + } + } + SetActivity(); + } + + public new void Add(T item, CancellationToken cancellationToken) { + lock (_lock) { + if (!IsAddingCompleted) { + base.Add(item, cancellationToken); + } + } + SetActivity(); + } + + public new IEnumerable GetConsumingEnumerable() { + return GetConsumingEnumerable(CancellationToken.None); + } + + public new IEnumerable GetConsumingEnumerable(CancellationToken cancellationToken) { + T item; + while (!IsCompleted && SafeTryTake(out item, 0, cancellationToken)) { + yield return item; + } + } + + private bool SafeTryTake(out T item, int time, CancellationToken cancellationToken) { + try { + if (!cancellationToken.IsCancellationRequested && Count > 0) { + return TryTake(out item, time, cancellationToken); + } + } catch { + // if this throws, that just means that we're done. (ie, canceled) + } finally { + SetActivity(); + } + item = default(T); + return false; + } + + public IEnumerable GetBlockingEnumerable() { + return GetBlockingEnumerable(CancellationToken.None); + } + + public IEnumerable GetBlockingEnumerable(CancellationToken cancellationToken) { + return _blockingEnumerable ?? (_blockingEnumerable = SafeGetBlockingEnumerable(cancellationToken).ReEnumerable()); + } + + private IEnumerable SafeGetBlockingEnumerable(CancellationToken cancellationToken) { + while (!IsCompleted && !cancellationToken.IsCancellationRequested) { + T item; + if (SafeTryTake(out item, -1, cancellationToken)) { + yield return item; + } + else { + _activity.Wait(10,cancellationToken); + //cancellationToken.WaitHandle.WaitOne(10); + } + } + } + + public new void CompleteAdding() { + lock (_lock) { + base.CompleteAdding(); + } + SetActivity(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Collections/CancellableEnumerator.cs b/src/Microsoft.PackageManagement/Utility/Collections/CancellableEnumerator.cs new file mode 100644 index 000000000..3b20615a5 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Collections/CancellableEnumerator.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Collections { + using System; + using System.Collections; + using System.Collections.Generic; + using System.Threading; + + internal class CancellableEnumerator : IEnumerator, ICancellableEnumerator { + private IEnumerator _enumerator; + private readonly CancellationTokenSource _cancellationTokenSource; + + public CancellableEnumerator(CancellationTokenSource cts, IEnumerator enumerator) { + _enumerator = enumerator; + _cancellationTokenSource = cts; + } + + public void Cancel() { + _cancellationTokenSource.Cancel(); + } + + public bool MoveNext() { + // if the collection has been cancelled, then don't advance anymore. + return !_cancellationTokenSource.IsCancellationRequested && _enumerator.MoveNext(); + } + + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void Reset() { + _enumerator.Reset(); + } + + public T Current { + get { + return (T)_enumerator.Current; + } + } + + object IEnumerator.Current { + get { + return _enumerator.Current; + } + } + + protected virtual void Dispose(bool disposing) { + if (disposing) { + if (_enumerator is IDisposable) { + (_enumerator as IDisposable).Dispose(); + } + _enumerator = null; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Collections/EnumerableExtensions.cs b/src/Microsoft.PackageManagement/Utility/Collections/EnumerableExtensions.cs new file mode 100644 index 000000000..07883ab4a --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Collections/EnumerableExtensions.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Collections { + using System; + using System.Collections.Generic; + using System.Linq; + + internal static class EnumerableExtensions { + /// + /// Returns a ReEnumerable wrapper around the collection which timidly (cautiously) pulls items + /// but still allows you to to re-enumerate without re-running the query. + /// + /// + /// + /// + public static MutableEnumerable ReEnumerable(this IEnumerable collection) { + if (collection == null) { + return new ReEnumerable(Enumerable.Empty()); + } + return collection as MutableEnumerable ?? new ReEnumerable(collection); + } + + public static IEnumerable FilterWithFinalizer(this IEnumerable source, Func predicate, Action onFilterAction) { + foreach (var i in source) { + if (predicate(i)) { + onFilterAction(i); + } else { + yield return i; + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Collections/FilenameEqualityComparer.cs b/src/Microsoft.PackageManagement/Utility/Collections/FilenameEqualityComparer.cs new file mode 100644 index 000000000..10edc215a --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Collections/FilenameEqualityComparer.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Collections { + internal enum PathCompareOption { + Full, + File, + FileWithoutExtension, + Extension, + Nuget + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Collections/ICancellableEnumerator.cs b/src/Microsoft.PackageManagement/Utility/Collections/ICancellableEnumerator.cs new file mode 100644 index 000000000..66db266f0 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Collections/ICancellableEnumerator.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Collections { + using System.Collections.Generic; + + internal interface ICancellableEnumerator : IEnumerator { + void Cancel(); + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Collections/List.cs b/src/Microsoft.PackageManagement/Utility/Collections/List.cs new file mode 100644 index 000000000..5f2f8c72d --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Collections/List.cs @@ -0,0 +1,88 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Collections { + using System; + using System.Collections.Generic; + + public class List : List> { + public void Add(TKey key, TValue value) { + Add(new KeyValuePair(key, value)); + } + + public int IndexOf(TKey key, TValue value) { + for (var i = 0; i < Count; i++) { + if ((ReferenceEquals(this[i].Key, key) || this[i].Key.Equals(key)) && (ReferenceEquals(this[i].Value, value) || this[i].Value.Equals(value))) { + return i; + } + } + return -1; + } + + public bool Contains(TKey key, TValue value) { + return IndexOf(key, value) > -1; + } + + public bool Remove(TKey key, TValue value) { + var i = IndexOf(key, value); + if (i > -1) { + RemoveAt(i); + return true; + } + return false; + } + } + + public class List : List> { + public void Add(T1 p1, T2 p2, T3 p3) { + Add(new Tuple(p1, p2, p3)); + } + + public bool Contains(T1 p1, T2 p2, T3 p3) { + return Contains(new Tuple(p1, p2, p3)); + } + + public bool Remove(T1 p1, T2 p2, T3 p3) { + return Remove(new Tuple(p1, p2, p3)); + } + } + + public class List : List> { + public void Add(T1 p1, T2 p2, T3 p3, T4 p4) { + Add(new Tuple(p1, p2, p3, p4)); + } + + public bool Contains(T1 p1, T2 p2, T3 p3, T4 p4) { + return Contains(new Tuple(p1, p2, p3, p4)); + } + + public bool Remove(T1 p1, T2 p2, T3 p3, T4 p4) { + return Remove(new Tuple(p1, p2, p3, p4)); + } + } + + public class List : List> { + public void Add(T1 p1, T2 p2, T3 p3, T4 p4, T5 p5) { + Add(new Tuple(p1, p2, p3, p4, p5)); + } + + public bool Contains(T1 p1, T2 p2, T3 p3, T4 p4, T5 p5) { + return Contains(new Tuple(p1, p2, p3, p4, p5)); + } + + public bool Remove(T1 p1, T2 p2, T3 p3, T4 p4, T5 p5) { + return Remove(new Tuple(p1, p2, p3, p4, p5)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Collections/MutableEnumerable.cs b/src/Microsoft.PackageManagement/Utility/Collections/MutableEnumerable.cs new file mode 100644 index 000000000..a5ae93588 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Collections/MutableEnumerable.cs @@ -0,0 +1,99 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Collections { + using System.Collections; + using System.Collections.Generic; + + internal abstract class MutableEnumerable : IEnumerable { + private List _list; + + protected List List { + get { + if (_list == null) { + lock (this) { + if (_list == null) { + _list = new List(); + } + } + } + return _list; + } + } + + public IEnumerable> GetEnumerators(int copies) { + for (var i = 0; i < copies; i++) { + yield return GetEnumerator(); + } + } + + protected abstract bool ItemExists(int index); + + internal class Enumerator : IEnumerator { + private MutableEnumerable _collection; + private int _index = -1; + + internal Enumerator(MutableEnumerable collection) { + _collection = collection; + } + + #region IEnumerator Members + + public TT Current { + get { + return _collection.List[_index]; + } + } + + public void Dispose() { + _collection = null; + } + + object IEnumerator.Current { + get { + return Current; + } + } + + public bool MoveNext() { + _index++; + return _collection.ItemExists(_index); + } + + public void Reset() { + _index = -1; + } + + public IEnumerator Clone() { + return new Enumerator(_collection) { + _index = _index + }; + } + + #endregion + } + + #region IEnumerable Members + + public IEnumerator GetEnumerator() { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Collections/OrderedDictionary.cs b/src/Microsoft.PackageManagement/Utility/Collections/OrderedDictionary.cs new file mode 100644 index 000000000..1191fe418 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Collections/OrderedDictionary.cs @@ -0,0 +1,225 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Collections { + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Linq; + + internal class OrderedDictionary : OrderedDictionary, IDictionary { + public new IEnumerator> GetEnumerator() { + return new KvpEnumerator(base.GetEnumerator()); + } + + public void Add(KeyValuePair item) { + base.Add(item.Key, item.Value); + } + + public bool Contains(KeyValuePair item) { + return base.Contains(item.Key) && item.Value.Equals(base[item.Key]); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) { + if (array == null) { + throw new ArgumentNullException("array"); + } + for (var e = GetEnumerator(); e.MoveNext();) { + array[arrayIndex++] = e.Current; + } + } + + public bool Remove(KeyValuePair item) { + if (Contains(item)) { + base.Remove(item.Key); + return true; + } + return false; + } + + public bool ContainsKey(TKey key) { + return base.Contains(key); + } + + public void Add(TKey key, TValue value) { + base.Add(key, value); + } + + public bool Remove(TKey key) { + if (base.Contains(key)) { + base.Remove(key); + return true; + } + return false; + } + + public bool TryGetValue(TKey key, out TValue value) { + if (base.Contains(key)) { + value = (TValue)base[key]; + return true; + } + value = default(TValue); + return false; + } + + public TValue this[TKey key] { + get { + return (TValue)base[key]; + } + set { + base[key] = value; + } + } + + public new ICollection Keys { + get { + return new KeyCollection(this); + } + } + + public new ICollection Values { + get { + return new ValueCollection(this); + } + } + + internal class KeyCollection : ICollection { + private readonly OrderedDictionary _dictionary; + + public KeyCollection(OrderedDictionary dictionary) { + _dictionary = dictionary; + } + + public IEnumerator GetEnumerator() { + return _dictionary.Keys.Cast().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + + public void Add(TKey item) { + throw new NotImplementedException(); + } + + public void Clear() { + throw new NotImplementedException(); + } + + public bool Contains(TKey item) { + return _dictionary.Contains(item); + } + + public void CopyTo(TKey[] array, int arrayIndex) { + _dictionary.Keys.CopyTo(array, arrayIndex); + } + + public bool Remove(TKey item) { + throw new NotImplementedException(); + } + + public int Count { + get { + return _dictionary.Keys.Count; + } + } + + public bool IsReadOnly { + get { + return true; + } + } + } + + internal class KvpEnumerator : IEnumerator> { + private readonly IDictionaryEnumerator _enumerator; + + internal KvpEnumerator(IDictionaryEnumerator e) { + _enumerator = e; + } + + public void Dispose() { + } + + public bool MoveNext() { + return _enumerator.MoveNext(); + } + + public void Reset() { + _enumerator.Reset(); + } + + public KeyValuePair Current { + get { + return new KeyValuePair((TKey)_enumerator.Key, (TValue)_enumerator.Value); + } + } + + object IEnumerator.Current { + get { + return Current; + } + } + } + + internal class ValueCollection : ICollection { + private readonly OrderedDictionary _dictionary; + + public ValueCollection(OrderedDictionary dictionary) { + _dictionary = dictionary; + } + + public IEnumerator GetEnumerator() { + return _dictionary.Values.Cast().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + + public void Add(TValue item) { + throw new NotImplementedException(); + } + + public void Clear() { + throw new NotImplementedException(); + } + + public bool Contains(TValue item) { + return _dictionary.Values.Cast().Contains(item); + } + + public void CopyTo(TValue[] array, int arrayIndex) { + _dictionary.Values.CopyTo(array, arrayIndex); + } + + public bool Remove(TValue item) { + throw new NotImplementedException(); + } + + public int Count { + get { + return _dictionary.Values.Count; + } + } + + public bool IsReadOnly { + get { + return true; + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Collections/PathEqualityComparer.cs b/src/Microsoft.PackageManagement/Utility/Collections/PathEqualityComparer.cs new file mode 100644 index 000000000..b1085516a --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Collections/PathEqualityComparer.cs @@ -0,0 +1,72 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Collections { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.IO; + + internal class PathEqualityComparer : IEqualityComparer { + private readonly PathCompareOption _option; + + public PathEqualityComparer(PathCompareOption option) { + _option = option; + } + + [SuppressMessage("Microsoft.Globalization", "CA1309:UseOrginalStringComparison")] + public bool Equals(string x, string y) { +#if !CORECLR + return string.Compare(ComparePath(x), ComparePath(y), StringComparison.InvariantCultureIgnoreCase) == 0; +#else + return string.Compare(ComparePath(x), ComparePath(y), StringComparison.CurrentCultureIgnoreCase) == 0; +#endif + } + + public int GetHashCode(string obj) { + return ComparePath(obj).ToUpperInvariant().GetHashCode(); + } + + private string ComparePath(string path) { + if (string.IsNullOrWhiteSpace(path)) { + return string.Empty; + } + + try { + switch (_option) + { + case PathCompareOption.Full: + return Path.GetFullPath(path); + case PathCompareOption.File: + return Path.GetFileName(path); + case PathCompareOption.FileWithoutExtension: + return Path.GetFileNameWithoutExtension(path); + case PathCompareOption.Extension: + return Path.GetExtension(path); + case PathCompareOption.Nuget: + //we changed nuget-anycpu.exe to Microsoft.PackageManagement.NuGetProvider as of 2.8.5.201. Without loading them, we have no way to know + //if they are all the nugetprovider. + //In order to load the latest version of the nuget provider, we need to add the code below. + var filename = Path.GetFileNameWithoutExtension(path); + return filename.Equals("nuget-anycpu", StringComparison.OrdinalIgnoreCase) ? "Microsoft.PackageManagement.NuGetProvider" : filename; + } + } + catch (Exception) { + //GetFullPath() can throw for the bad path or unsupported path format, e.g. PS:MydriveFolder. + //we should not throw in this case. Intead return string.Empty to indicate not matching + } + return string.Empty; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Collections/ReEnumerable.cs b/src/Microsoft.PackageManagement/Utility/Collections/ReEnumerable.cs new file mode 100644 index 000000000..063805ccf --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Collections/ReEnumerable.cs @@ -0,0 +1,77 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Collections { + using System.Collections.Generic; + using System.Linq; + + internal class ReEnumerable : MutableEnumerable { + private IEnumerator _sourceIterator; + private readonly IEnumerable _source; + + public ReEnumerable(IEnumerable source) { + _source = source ?? new T[0]; + } + + public ReEnumerable(IEnumerator sourceIterator) { + _source = null; + _sourceIterator = sourceIterator; + } + + public T this[int index] { + get { + if (ItemExists(index)) { + return List[index]; + } + return default(T); + } + } + + public int Count { + get { + return this.Count(); + } + } + + protected override bool ItemExists(int index) { + if (index < List.Count) { + return true; + } + + lock (this) { + if (_sourceIterator == null) { + _sourceIterator = _source.GetEnumerator(); + } + + try { + while (_sourceIterator.MoveNext()) { + List.Add(_sourceIterator.Current); + if (index < List.Count) { + return true; + } + } + } catch { + // if the _sourceIterator is cancelled + // then MoveNext() will throw; that's ok + // that just means we're done + } + } + return false; + } + + public MutableEnumerable Concat(IEnumerable additionalItems) { + return Enumerable.Concat(this, additionalItems).ReEnumerable(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Extensions/CollectionExtensions.cs b/src/Microsoft.PackageManagement/Utility/Extensions/CollectionExtensions.cs new file mode 100644 index 000000000..8fa90090d --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Extensions/CollectionExtensions.cs @@ -0,0 +1,175 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Extensions { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using System.Threading.Tasks; + using Collections; + + internal static class CollectionExtensions { + private static readonly MethodInfo _castMethod = typeof (Enumerable).GetMethod("Cast"); + private static readonly MethodInfo _toArrayMethod = typeof (Enumerable).GetMethod("ToArray"); + private static readonly IDictionary _castMethods = new Dictionary(); + private static readonly IDictionary _toArrayMethods = new Dictionary(); + + /// + /// Determines whether the collection object is either null or an empty collection. + /// + /// + /// The collection. + /// + /// true if [is null or empty] [the specified collection]; otherwise, false . + /// + /// + /// + public static bool IsNullOrEmpty(this IEnumerable collection) { + return collection == null || !collection.Any(); + } + + /// + /// Concatenates a single item to an IEnumerable + /// + /// + /// + /// + /// + public static IEnumerable ConcatSingleItem(this IEnumerable enumerable, T item) { + return enumerable.Concat(new[] { + item + }); + } + + public static IEnumerable WhereNotNull(this IEnumerable enumerable) { + return enumerable == null ? Enumerable.Empty() : enumerable.Where(each => (object)each != null); + } + + public static IEnumerable ToIEnumerable(this IEnumerator enumerator) { + if (enumerator != null) { + while (enumerator.MoveNext()) { + yield return enumerator.Current; + } + } + } + + public static IEnumerable Concat(this IEnumerable set1, IEnumerator set2) { + var s1 = set1 ?? Enumerable.Empty(); + var s2 = set2 == null ? Enumerable.Empty() : set2.ToIEnumerable(); + return s1.Concat(s2); + } + + public static IEnumerable SingleItemAsEnumerable(this T item) { + return new T[] { + item + }; + } + + public static void AddRangeLocked(this List list, IEnumerable items) { + if (list == null) { + throw new ArgumentNullException("list"); + } + lock (list) { + list.AddRange(items); + } + } + + public static void ParallelForEach(this IEnumerable enumerable, Action action) { + var items = enumerable.ReEnumerable(); + var first = items.FirstOrDefault(); + if (first != null) { + object second = items.Skip(1).FirstOrDefault(); + if (second != null) { + Parallel.ForEach(items, new ParallelOptions { + MaxDegreeOfParallelism = -1, + TaskScheduler = new ThreadPerTaskScheduler() + }, action); + } else { + action(first); + } + } + } + + public static void SerialForEach(this IEnumerable enumerable, Action action) { + var items = enumerable.ReEnumerable(); + object first = items.FirstOrDefault(); + if (first != null) { + object second = items.Skip(1).FirstOrDefault(); + if (second != null) { + foreach (var item in items) { + action(item); + } + } else { + action(items.FirstOrDefault()); + } + } + } + + public static IEnumerable AsyncForEach(this IEnumerable enumerable, Action action) { + return enumerable.Select(i => Task.Factory.StartNew(() => action(i))); + } + + public static void WaitAll(this IEnumerable tasks) { + Task.WaitAll(tasks.ToArray()); + } + + public static object ToIEnumerableT(this IEnumerable enumerable, Type elementType) { + return _castMethods.GetOrAdd(elementType, () => _castMethod.MakeGenericMethod(elementType)).Invoke(null, new object[] {enumerable}); + } + + public static object ToArrayT(this IEnumerable enumerable, Type elementType) { + return _toArrayMethods.GetOrAdd(elementType, () => _toArrayMethod.MakeGenericMethod(elementType)).Invoke(null, new[] {enumerable.ToIEnumerableT(elementType)}); + } + + /// + /// returns the index position where the predicate matches the value + /// + /// + /// + /// + /// + public static int IndexWhere(this IEnumerable source, Func predicate) { + if (source != null && predicate != null) { + var index = -1; + if (source.Any(element => predicate(element, ++index))) { + return index; + } + } + return -1; + } + + /// + /// returns the index position where the predicate matches the value + /// + /// + /// + /// + /// + public static int IndexWhere(this IEnumerable source, Func predicate) { + if (source != null && predicate != null) { + var index = -1; + if (source.Any(element => { + ++index; + return predicate(element); + })) { + return index; + } + } + return -1; + } + } + + // Provides a task scheduler that dedicates a thread per task. +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Extensions/DelegateExtensions.cs b/src/Microsoft.PackageManagement/Utility/Extensions/DelegateExtensions.cs new file mode 100644 index 000000000..966cb61e7 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Extensions/DelegateExtensions.cs @@ -0,0 +1,141 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Extensions { + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Reflection; + using System.Reflection.Emit; + using Plugin; + using Extensions; + + internal static class DelegateExtensions { + private static readonly Dictionary _emptyDelegates = new Dictionary(); + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "shhh.")] + internal static Type GetDelegateReturnType(this Delegate delegateInstance) { + return GetDelegateReturnType(delegateInstance.GetType()); + } + + internal static Type GetDelegateReturnType(this Type delegateType) { + if (delegateType.GetTypeInfo().BaseType != typeof (MulticastDelegate)) { + throw new ArgumentException("Not a delegate."); + } + + var invoke = delegateType.GetMethod("Invoke"); + if (invoke == null) { + throw new ArgumentException("Not a delegate."); + } + return invoke.ReturnType; + } + + internal static IEnumerable GetDelegateParameterTypes(this Type delegateType) { + if (delegateType.GetTypeInfo().BaseType != typeof (MulticastDelegate)) { + throw new ArgumentException("Not a delegate."); + } + + var invoke = delegateType.GetMethod("Invoke"); + if (invoke == null) { + throw new ArgumentException("Not a delegate."); + } + + return invoke.GetParameters().Select(each => each.ParameterType); + } + + internal static IEnumerable GetDelegateParameterNames(this Type delegateType) { + if (delegateType.GetTypeInfo().BaseType != typeof (MulticastDelegate)) { + throw new ArgumentException("Not a delegate."); + } + + var invoke = delegateType.GetMethod("Invoke"); + if (invoke == null) { + throw new ArgumentException("Not a delegate."); + } + + return invoke.GetParameters().Select(each => each.Name); + } + + internal static Type[] GetParameterTypes(this MethodInfo methodInfo) { + return methodInfo.GetParameters().Select(each => each.ParameterType).ToArray(); + } + + internal static bool IsDelegateAssignableFromMethod(this Type delegateType, MethodInfo methodInfo) { + if (delegateType == null || methodInfo == null) { + return false; + } + + // are the return types the same? + if (methodInfo.ReturnType != delegateType.GetDelegateReturnType() && !methodInfo.ReturnType.IsAssignableFrom(delegateType.GetDelegateReturnType())) { + return false; + } + + + + if (!delegateType.GetDelegateParameterTypes().SequenceEqual(methodInfo.GetParameterTypes(), AssignableTypeComparer.Instance)) { + return false; + } + return true; + } + + internal static bool IsDelegateAssignableFromDelegate(this Type delegateType, Type candidateDelegateType) { + if (delegateType == null || candidateDelegateType == null) { + return false; + } + + // ensure both are actually delegates + if (delegateType.GetTypeInfo().BaseType != typeof (MulticastDelegate) || candidateDelegateType.GetTypeInfo().BaseType != typeof (MulticastDelegate)) { + return false; + } + + // are the return types the same? + if (candidateDelegateType.GetDelegateReturnType() != delegateType.GetDelegateReturnType() && !delegateType.GetDelegateReturnType().IsAssignableFrom(candidateDelegateType.GetDelegateReturnType())) { + return false; + } + + // are all the parameters the same types? + if (!delegateType.GetDelegateParameterTypes().SequenceEqual(candidateDelegateType.GetDelegateParameterTypes(), AssignableTypeComparer.Instance)) { + return false; + } + return true; + } + + internal static Delegate CreateEmptyDelegate(this Type delegateType) { + if (delegateType == null) { + throw new ArgumentNullException("delegateType"); + } + if (delegateType.GetTypeInfo().BaseType != typeof (MulticastDelegate)) { + throw new ArgumentException("must be a delegate", "delegateType"); + } + + return _emptyDelegates.GetOrAdd(delegateType, () => { + var delegateReturnType = delegateType.GetDelegateReturnType(); + + var dynamicMethod = new DynamicMethod(string.Empty, delegateReturnType, delegateType.GetDelegateParameterTypes().ToArray()); + var il = dynamicMethod.GetILGenerator(); + + if (delegateReturnType.FullName != "System.Void") { + if (delegateReturnType.GetTypeInfo().IsValueType) { + il.Emit(OpCodes.Ldc_I4, 0); + } else { + il.Emit(OpCodes.Ldnull); + } + } + il.Emit(OpCodes.Ret); + return dynamicMethod.CreateDelegate(delegateType); + }); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Extensions/DictionaryExtensions.cs b/src/Microsoft.PackageManagement/Utility/Extensions/DictionaryExtensions.cs new file mode 100644 index 000000000..d24b34989 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Extensions/DictionaryExtensions.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Extensions { + using System; + using System.Collections.Generic; + + internal static class DictionaryExtensions { + public static Tuple Add(this IDictionary> dictionary, TKey key, T1 v1, T2 v2) { + var item = new Tuple(v1, v2); + dictionary.Add(key, item); + return item; + } + + public static void RemoveSafe(this IDictionary dictionary, TKey key) { + lock (dictionary) { + if (dictionary.ContainsKey(key)) { + dictionary.Remove(key); + } + } + } + + public static TValue AddOrSet(this IDictionary dictionary, TKey key, TValue value) { + lock (dictionary) { + if (dictionary.ContainsKey(key)) { + dictionary[key] = value; + } else { + dictionary.Add(key, value); + } + } + return value; + } + + public static TValue GetOrAdd(this IDictionary dictionary, TKey key, Func valueFunction) { + lock (dictionary) { + return dictionary.ContainsKey(key) ? dictionary[key] : dictionary.AddOrSet(key, valueFunction()); + } + } + + public static TValue GetOrSetIfDefault(this IDictionary dictionary, TKey key, Func valueFunction) { + lock (dictionary) { + return !dictionary.ContainsKey(key) || ((object)default(TValue) == (object)dictionary[key]) ? dictionary.AddOrSet(key, valueFunction()) : dictionary[key]; + } + } + + public static TValue TryPullValue(this IDictionary dictionary, TKey key) { + if (dictionary.ContainsKey(key)) { + var v = dictionary[key]; + dictionary.Remove(key); + return v; + } + return default(TValue); + } + + public static Dictionary ToDictionaryNicely(this IEnumerable source, Func keySelector, Func elementSelector, IEqualityComparer comparer) { + if (source == null) { + throw new ArgumentNullException("source"); + } + if (keySelector == null) { + throw new ArgumentNullException("keySelector"); + } + if (elementSelector == null) { + throw new ArgumentNullException("elementSelector"); + } + + var d = new Dictionary(comparer); + foreach (var element in source) { + var key = keySelector(element); + if (key != null) { + d.AddOrSet(key, elementSelector(element)); + } + } + + return d; + } + + public static TValue Get(this IDictionary dictionary, TKey key) { + return dictionary.ContainsKey(key) ? dictionary[key] : default(TValue); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Extensions/EqualityComparer.cs b/src/Microsoft.PackageManagement/Utility/Extensions/EqualityComparer.cs new file mode 100644 index 000000000..daeaa6046 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Extensions/EqualityComparer.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Extensions { + using System; + using System.Collections.Generic; + + internal class EqualityComparer : IEqualityComparer { + private readonly Func _compareFn; + private readonly Func _hashFn; + + public EqualityComparer(Func compareFn, Func hashFn) { + _compareFn = compareFn; + _hashFn = hashFn; + } + + public EqualityComparer(Func compareFn) { + _compareFn = compareFn; + _hashFn = (obj) =>obj.GetHashCode(); + } + + public bool Equals(T x, T y) { + return _compareFn(x, y); + } + + public int GetHashCode(T obj) { + return _hashFn(obj); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Extensions/ExceptionExtensions.cs b/src/Microsoft.PackageManagement/Utility/Extensions/ExceptionExtensions.cs new file mode 100644 index 000000000..dc2c842af --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Extensions/ExceptionExtensions.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Extensions { + using System; + using System.Globalization; + using Platform; + + internal static class ExceptionExtensions { + public static void Dump(this Exception e) { + var text = string.Format(CultureInfo.CurrentCulture, "{0}/{1}\r\n{2}", e.GetType().Name, e.Message, e.StackTrace); + // for now, this is the only way we'll see exceptions in the wild. + NativeMethods.OutputDebugString(text); + } + +#if DETAILED_DEBUG + private static DateTime startTime = DateTime.Now; + public static T DumpTime(this T nothing) { + StackTrace s = new StackTrace(true); + var f = s.GetFrame(1); + + Console.WriteLine(" OFFSET: \r\n {0}:{1}:\r\n {2}\r\n Time:[{3}] ",f.GetFileName(), f.GetFileLineNumber(), f.GetMethod(), DateTime.Now.Subtract(startTime).TotalMilliseconds); + return nothing; + } +#endif + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Extensions/FilesystemExtensions.cs b/src/Microsoft.PackageManagement/Utility/Extensions/FilesystemExtensions.cs new file mode 100644 index 000000000..2c97c5048 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Extensions/FilesystemExtensions.cs @@ -0,0 +1,268 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Extensions { + using System; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Reflection; + using System.Text; + using System.Text.RegularExpressions; + using Platform; + + internal static class FilesystemExtensions { + private static readonly char[] _pathCharacters = "/\\".ToCharArray(); + private static int _counter = Process.GetCurrentProcess().Id << 16; + public static string OriginalTempFolder; + + static FilesystemExtensions() { + OriginalTempFolder = OriginalTempFolder ?? Path.GetTempPath(); + ResetTempFolder(); + } + + public static string TempPath {get; private set;} + + public static int Counter { + get { + return ++_counter; + } + } + + public static string CounterHex { + get { + return Counter.ToString("x8", CultureInfo.CurrentCulture); + } + } + + public static bool LooksLikeAFilename(this string text) { + return text.IndexOfAny(_pathCharacters) > -1; + } + + public static void TryHardToDelete(this string location) { + if (Directory.Exists(location)) { + try { + Directory.Delete(location, true); + } catch { + // didn't take, eh? + } + } + + if (File.Exists(location)) { + try { + File.Delete(location); + } catch { + // didn't take, eh? + } + } + + // if it is still there, move and mark it. + if (File.Exists(location) || Directory.Exists(location)) { + try { + // move the file to the tmp file + // and tell the OS to remove it next reboot. + var tmpFilename = GenerateTemporaryFileOrDirectoryNameInTempDirectory() + ".delete_me"; // generates a unique filename but not a file! + MoveFileOverwrite(location, tmpFilename); + + if (File.Exists(location) || Directory.Exists(location)) { + // of course, if the tmpFile isn't on the same volume as the location, this doesn't work. + // then, last ditch effort, let's rename it in the current directory + // and then we can hide it and mark it for cleanup . + tmpFilename = Path.Combine(Path.GetDirectoryName(location), "tmp." + CounterHex + "." + Path.GetFileName(location) + ".delete_me"); + MoveFileOverwrite(location, tmpFilename); + if (File.Exists(tmpFilename) || Directory.Exists(location)) { + // hide the file for convenience. + File.SetAttributes(tmpFilename, File.GetAttributes(tmpFilename) | FileAttributes.Hidden); + } + } + + // Now we mark the locked file to be deleted upon next reboot (or until another coapp app gets there) + MoveFileOverwrite(File.Exists(tmpFilename) ? tmpFilename : location, null); + } catch { + // really. Hmmm. + } + + if (File.Exists(location)) { + // err("Unable to forcably remove file '{0}'. This can't be good.", location); + } + } + return; + } + + /// + /// File move abstraction that can be implemented to handle non-windows platforms + /// + /// + /// + public static void MoveFileOverwrite(string sourceFile, string destinationFile) { + NativeMethods.MoveFileEx(sourceFile, destinationFile, MoveFileFlags.ReplaceExisting); + } + + public static void MoveFileAtNextBoot(string sourceFile, string destinationFile) { + NativeMethods.MoveFileEx(sourceFile, destinationFile, MoveFileFlags.DelayUntilReboot); + } + + /// + /// Create a temporary file name in the temp directory so we can move file that we cannot delete over + /// + /// + public static string GenerateTemporaryFileOrDirectoryNameInTempDirectory() { + string name = string.Empty; + + do + { + // keep doing this until we get a file name that does not exist + name = Path.Combine(TempPath, Path.GetRandomFileName()); + } + while (File.Exists(name) || Directory.Exists(name)); + + return name; + } + + public static void ResetTempFolder() { + // set the temporary folder to be a child of the User temporary folder + // based on the application name + var appName = typeof(FilesystemExtensions).GetTypeInfo().Assembly.GetName().Name; + if (OriginalTempFolder.IndexOf(appName, StringComparison.CurrentCultureIgnoreCase) == -1) { + var appTempPath = Path.Combine(OriginalTempFolder, appName); + if (!Directory.Exists(appTempPath)) { + Directory.CreateDirectory(appTempPath); + } + + TempPath = appTempPath; + + // delete all the directories in Microsoft.PackageManagement folder in temp that is older than 2 days + foreach (var directory in Directory.EnumerateDirectories(TempPath)) + { + try + { + var dirInfo = new DirectoryInfo(directory); + + // delete all directories older than 2 days + if (dirInfo.CreationTime < DateTime.Now.AddDays(-2)) + { + Directory.Delete(directory, true); + } + } + catch { } + } + + // delete all filese that is older than 2 days + foreach (var filePath in Directory.EnumerateFiles(TempPath)) + { + try + { + var fileInfo = new FileInfo(filePath); + + // delete anything older than 2 days + if (fileInfo.CreationTime < DateTime.Now.AddDays(-2)) + { + File.Delete(filePath); + } + } + catch { } + } + } + + TempPath = TempPath ?? OriginalTempFolder; + } + + /// + /// This takes a string that is representative of a filename and tries to create a path that can be considered the + /// 'canonical' path. path on drives that are mapped as remote shares are rewritten as their \\server\share\path + /// + /// + public static string CanonicalizePath(this string path, bool isPotentiallyRelativePath) { + Uri pathUri = null; + try { + pathUri = new Uri(path); + if (!pathUri.IsFile) { + // perhaps try getting the fullpath + try { + pathUri = new Uri(Path.GetFullPath(path)); + } catch { + return null; + } + } + + // is this a unc path? + if (string.IsNullOrWhiteSpace(pathUri.Host)) { + // no, this is a drive:\path path + // use API to resolve out the drive letter to see if it is a remote + var drive = pathUri.Segments[1].Replace('/', '\\'); // the zero segment is always just '/' + + var sb = new StringBuilder(512); + var size = sb.Capacity; + + var error = NativeMethods.WNetGetConnection(drive, sb, ref size); + if (error == 0) { + if (pathUri.Segments.Length > 2) { + return pathUri.Segments.Skip(2).Aggregate(sb.ToString().Trim(), (current, item) => current + item); + } + } + } + // not a remote (or resovably-remote) path or + // it is already a path that is in it's correct form (via localpath) + return pathUri.LocalPath; + } catch (UriFormatException) { + // we could try to see if it is a relative path... + if (isPotentiallyRelativePath) { + return CanonicalizePath(Path.GetFullPath(path), false); + } + return null; + } + } + + public static byte[] ReadBytes(this string path, int maxLength) { + if (path.FileExists()) { + try { + var buffer = new byte[Math.Min(new FileInfo(path).Length, maxLength)]; + using (var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { + file.Read(buffer, 0, buffer.Length); + } + + return buffer; + } catch { + // not openable. whatever. + } + } + return new byte[0]; + } + + public static bool FileExists(this string path) { + if (!string.IsNullOrWhiteSpace(path)) { + try { + return File.Exists(CanonicalizePath(path, true)); + } catch { + } + } + return false; + } + + public static bool DirectoryExists(this string path) { + if (!string.IsNullOrWhiteSpace(path)) { + try { + return Directory.Exists(CanonicalizePath(path, true)); + } catch { + } + } + return false; + } + + public static string MakeSafeFileName(this string input) { + return new Regex(@"-+").Replace(new Regex(@"[^\d\w\[\]_\-\.\ ]").Replace(input, "-"), "-").Replace(" ", ""); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Extensions/HashtableExtensions.cs b/src/Microsoft.PackageManagement/Utility/Extensions/HashtableExtensions.cs new file mode 100644 index 000000000..b518804b8 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Extensions/HashtableExtensions.cs @@ -0,0 +1,70 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Extensions { + using System.Collections; + using System.Collections.Generic; + using System.Linq; + + internal static class HashtableExtensions { + public static bool IsNullOrEmpty(this Hashtable hashtable) { + return hashtable == null || hashtable.Count == 0; + } + + public static IEnumerable GetStringCollection(this Hashtable hashtable, string path) { + if (hashtable.IsNullOrEmpty() || string.IsNullOrWhiteSpace(path)) { + return Enumerable.Empty(); + } + return hashtable.GetStringCollection(path.Split('/').Select(each => each.Trim()).ToArray()); + } + + public static IEnumerable GetStringCollection(this Hashtable hashtable, string[] paths) { + if (hashtable.IsNullOrEmpty() || paths.IsNullOrEmpty() || !hashtable.ContainsKey(paths[0])) { + return Enumerable.Empty(); + } + + if (paths.Length == 1) { + // looking for the actual result value + var items = hashtable[paths[0]] as IEnumerable; + if (items != null) { + return items.Select(each => each.ToString()); + } + + return new[] { + (hashtable[paths[0]] ?? "").ToString() + }; + } + + var item = hashtable[paths[0]] as Hashtable; + return item == null ? Enumerable.Empty() : item.GetStringCollection(paths.Skip(1).ToArray()); + } + + public static IEnumerable> Flatten(this Hashtable hashTable) { + foreach (var k in hashTable.Keys) { + var value = hashTable[k]; + if (value == null) { + continue; + } + + if (value is Hashtable) { + foreach (var kvp in (value as Hashtable).Flatten()) { + yield return new KeyValuePair(k.ToString() + "/" + kvp.Key, kvp.Value); + } + } else { + yield return new KeyValuePair(k.ToString(), value.ToString()); + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Extensions/Singleon.cs b/src/Microsoft.PackageManagement/Utility/Extensions/Singleon.cs new file mode 100644 index 000000000..7c5e28701 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Extensions/Singleon.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Extensions { + using System; + using System.Collections.Generic; + using System.Linq; + + internal static class Singleton { + private static readonly Dictionary _singletons = new Dictionary(); + + private static readonly int[] _seeds = { + 53, 379, 1301, 2333, 2357, 2539, 3121, 1009, 419, 3559 + }; + + /// + /// Create a pretty good hashcode using the hashcodes from a bunch of objects. + /// + public static int CreateHashCode(object[] keys) { + if (keys.Length == 0) { + return 0; + } + + var hash = _seeds.Zip(keys, (prime, obj) => prime*(obj == null ? 0 : obj.GetHashCode())).Aggregate((accum, each) => accum + each); + + return (keys.Length <= _seeds.Length) ? hash : keys.Skip(10).Aggregate(hash, (accum, obj) => accum + (obj == null ? 0 : obj.GetHashCode())); + } + + /// + /// Create a pretty good hashcode using the hashcodes from a bunch of objects. + /// + public static int CreateHashCode(object primaryKey, object[] keys) { + if (primaryKey == null) { + return CreateHashCode(keys); + } + + if (keys.Length == 0) { + return primaryKey.GetHashCode(); + } + + var hash = _seeds.Zip(keys, (prime, obj) => prime*(obj == null ? 0 : obj.GetHashCode())).Aggregate((accum, each) => accum + each) + primaryKey.GetHashCode(); + + return (keys.Length <= _seeds.Length) ? hash : keys.Skip(10).Aggregate(hash, (accum, obj) => accum + (obj == null ? 0 : obj.GetHashCode())); + } + + public static TResult GetOrAdd(Func newInstance, object primaryKey, params object[] keys) { + return _singletons.GetOrAdd(CreateHashCode(primaryKey, keys), newInstance); + } + + public static TResult Get(object primaryKey, params object[] keys) { + var hash = CreateHashCode(primaryKey, keys); + return _singletons.ContainsKey(hash) ? _singletons[hash] : default(TResult); + } + + public static TResult AddOrSet(TResult value, object primaryKey, params object[] keys) { + return _singletons.AddOrSet(CreateHashCode(primaryKey, keys), value); + } + + public static void Remove(object primaryKey, params object[] keys) { + _singletons.Remove(CreateHashCode(primaryKey, keys)); + } + } + + internal static class SingletonExtensions { + public static TResult Get(this object primaryKey, params object[] keys) { + return Singleton.Get(primaryKey, keys); + } + + public static TResult GetOrAdd(this object primaryKey, Func newInstance, params object[] keys) { + return Singleton.GetOrAdd(newInstance, primaryKey, keys); + } + + public static TResult AddOrSet(this object primaryKey, TResult value, params object[] keys) { + return Singleton.AddOrSet(value, primaryKey, keys); + } + + public static void Remove(this object primaryKey, params object[] keys) { + Singleton.Remove(primaryKey, keys); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Extensions/StringExtensions.cs b/src/Microsoft.PackageManagement/Utility/Extensions/StringExtensions.cs new file mode 100644 index 000000000..09ba727f8 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Extensions/StringExtensions.cs @@ -0,0 +1,382 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Extensions { + using System; + using System.Reflection; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Runtime.InteropServices; + using System.Security; + using System.Security.Cryptography; + using System.Text; + using System.Text.RegularExpressions; + using Collections; + + internal static class StringExtensions { + private static readonly char[] _wildcardCharacters = new[] { + '*', '?' + }; + + private static readonly Regex _escapeFilepathCharacters = new Regex(@"([\\|\$|\^|\{|\[|\||\)|\+|\.|\]|\}|\/])"); + + private static string FixMeFormat(string formatString, object[] args) { + return args.Aggregate(formatString.Replace('{', '\u00ab').Replace('}', '\u00bb'), (current, arg) => current + string.Format(CultureInfo.CurrentCulture, " \u00ab{0}\u00bb", arg)); + } + + public static IEnumerable Quote(this IEnumerable items) { + return items.Select(each => "'" + each + "'"); + } + + public static string JoinWithComma(this IEnumerable items) { + return items.JoinWith(","); + } + + public static string JoinWith(this IEnumerable items, string delimiter) { + return items.SafeAggregate((current, each) => current + delimiter + each); + } + + public static TSource SafeAggregate(this IEnumerable source, Func func) { + var src = source.ReEnumerable(); + if (source != null && src.Any()) { + return src.Aggregate(func); + } + return default(TSource); + } + +#if !CORECLR + /// + /// encrypts the given collection of bytes with the user key and salt + /// + /// The binary data. + /// The salt. + /// + /// + /// + public static IEnumerable ProtectBinaryForUser(this IEnumerable binaryData, string salt) { + var data = binaryData.ToArray(); + var s = salt.ToByteArray(); + try { + return ProtectedData.Protect(data, s, DataProtectionScope.CurrentUser); + } finally { + Array.Clear(data, 0, data.Length); + Array.Clear(s, 0, s.Length); + } + } + + /// + /// decrypts the given collection of bytes with the user key and salt returns an empty collection of bytes on failure + /// + /// The binary data. + /// The salt. + /// + /// + /// + public static IEnumerable UnprotectBinaryForUser(this IEnumerable binaryData, string salt) { + if (binaryData == null) { + return Enumerable.Empty(); + } + + try { + return ProtectedData.Unprotect(binaryData.ToArray(), salt.ToByteArray(), DataProtectionScope.CurrentUser); + } catch { + /* suppress */ + } + return Enumerable.Empty(); + } + + public static SecureString ToSecureString(this string password) { + if (password == null) { + throw new ArgumentNullException("password"); + } + + var ss = new SecureString(); + foreach (var ch in password.ToCharArray()) { + ss.AppendChar(ch); + } + + return ss; + } + + public static string ToProtectedString(this SecureString secureString, string salt) { + return Convert.ToBase64String(secureString.ToBytes().ProtectBinaryForUser(salt).ToArray()); + } + + public static SecureString FromProtectedString(this string str, string salt) { + return Convert.FromBase64String(str).UnprotectBinaryForUser(salt).ToUnicodeString().ToSecureString(); + } + + public static IEnumerable ToBytes(this SecureString securePassword) { + if (securePassword == null) { + throw new ArgumentNullException("securePassword"); + } + + var unmanagedString = Marshal.SecureStringToGlobalAllocUnicode(securePassword); + var ofs = 0; + + do { + var x = Marshal.ReadByte(unmanagedString, ofs++); + var y = Marshal.ReadByte(unmanagedString, ofs++); + if (x == 0 && y == 0) { + break; + } + // now we have two bytes! + yield return x; + yield return y; + } while (true); + + Marshal.ZeroFreeGlobalAllocUnicode(unmanagedString); + } + +#endif + + public static Version ToVersion(this string versionInput) { + if (string.IsNullOrWhiteSpace(versionInput)) { + return null; + } + Version result; + return Version.TryParse(versionInput, out result) ? result : null; + } + + public static bool ContainsIgnoreCase(this IEnumerable collection, string value) { + if (collection == null) { + return false; + } + return collection.Any(s => s.EqualsIgnoreCase(value)); + } + + public static bool ContainsAnyOfIgnoreCase(this IEnumerable collection, params object[] values) { + return collection.ContainsAnyOfIgnoreCase(values.Select(value => value == null ? null : value.ToString())); + } + + public static bool ContainsAnyOfIgnoreCase(this IEnumerable collection, IEnumerable values) { + if (collection == null) { + return false; + } + var set = values.ReEnumerable(); + + return collection.Any(set.ContainsIgnoreCase); + } + + private static Regex WildcardToRegex(string wildcard, string noEscapePrefix = "^") { + return new Regex(noEscapePrefix + _escapeFilepathCharacters.Replace(wildcard, "\\$1") + .Replace("?", @".") + .Replace("**", @"?") + .Replace("*", @"[^\\\/\<\>\|]*") + .Replace("?", @".*") + '$', RegexOptions.IgnoreCase); + } + + /// + /// Determines whether the specified input has wildcards. + /// + /// The input. + /// + /// true if the specified input has wildcards; otherwise, false . + /// + /// + /// + public static bool ContainsWildcards(this string input) { + return input.IndexOfAny(_wildcardCharacters) > -1; + } + + /// + /// Determines whether the input string is equals to the source string + /// ignoring a single / at the end + /// + /// + /// + /// + public static bool EqualsIgnoreEndSlash(this string source, string input) + { + return source.EqualsIgnoreCase(input) + || (string.Concat(source, "/")).EqualsIgnoreCase(input) + || (string.Concat(input, "/")).EqualsIgnoreCase(source); + } + + /// + /// Determines whether the input string contains the specified substring + /// + public static bool ContainsIgnoreCase(this string source, string input) { + return source.IndexOf(input, StringComparison.OrdinalIgnoreCase) >= 0; + } + + public static bool IsFile(this string input) { + if (string.IsNullOrWhiteSpace(input)) { + return false; + } + + try { + Uri result; + if (Uri.TryCreate(input, UriKind.Absolute, out result)) { + return result.IsFile; + } + } catch { + } + return false; + } + + public static bool IsWildcardMatch(this string input, string wildcardMask) { + if (input == null || string.IsNullOrWhiteSpace(wildcardMask)) { + return false; + } + return WildcardToRegex(wildcardMask).IsMatch(input); + } + + private static byte FromHexChar(this char c) { + if ((c >= 'a') && (c <= 'f')) { + return (byte)(c - 'a' + 10); + } + if ((c >= 'A') && (c <= 'F')) { + return (byte)(c - 'A' + 10); + } + if ((c >= '0') && (c <= '9')) { + return (byte)(c - '0'); + } + throw new ArgumentException("invalid hex char"); + } + + public static byte[] FromHex(this string hex) { + if (string.IsNullOrWhiteSpace(hex)) { + return new byte[0]; + } + + if ((hex.Length & 0x1) == 0x1) { + throw new ArgumentException("Length must be a multiple of 2"); + } + var input = hex.ToCharArray(); + var result = new byte[hex.Length >> 1]; + + for (var i = 0; i < input.Length; i += 2) { + result[i >> 1] = (byte)(((byte)(FromHexChar(input[i]) << 4)) | FromHexChar(input[i + 1])); + } + + return result; + } + + public static string FixVersion(this string versionString) { + if (!string.IsNullOrWhiteSpace(versionString)) { + if (versionString[0] == '.') { + // make sure we have a leading zero when someone says .5 + versionString = "0" + versionString; + } + + if (versionString.IndexOf('.') == -1) { + // make sure we make a 1 work like 1.0 + versionString = versionString + ".0"; + } + } + return versionString; + } + + // ReSharper disable InconsistentNaming + /// + /// Formats the specified format string. + /// + /// The format string. + /// The args. + /// + /// + /// + public static string format(this string formatString, params object[] args) { + if (args == null || args.Length == 0) { + return formatString; + } + + try { + var replacedByName = false; + // first, try to replace + formatString = new Regex(@"\$\{(?\w*?)\}").Replace(formatString, new MatchEvaluator((m) => { + var key = m.Groups["macro"].Value; + + var p = args[0].GetType().GetProperty(key); + if (p != null) { + replacedByName = true; + return p.GetValue(args[0], null).ToString(); + } + return "${{" + m.Groups["macro"].Value + "}}"; + })); + + // if it looks like it doesn't take parameters, (and yet we have args!) + // let's return a fix-me-format string. + if (!replacedByName && formatString.IndexOf('{') < 0) { + return FixMeFormat(formatString, args); + } + + return String.Format(CultureInfo.CurrentCulture, formatString, args); + } catch (Exception) { + // if we got an exception, let's at least return a string that we can use to figure out what parameters should have been matched vs what was passed. + return FixMeFormat(formatString, args); + } + } + + /// + /// Encodes the string as an array of UTF8 bytes. + /// + /// The text. + /// + /// + /// + internal static byte[] ToByteArray(this string text) { + return Encoding.UTF8.GetBytes(text); + } + + internal static string ToUnicodeString(this IEnumerable bytes) { + var data = bytes.ToArray(); + try { + return Encoding.Unicode.GetString(data); + } finally { + Array.Clear(data, 0, data.Length); + } + } + + public static bool IsTrue(this string text) { + return !string.IsNullOrWhiteSpace(text) && text.Equals("true", StringComparison.CurrentCultureIgnoreCase); + } + + public static bool? IsTruePreserveNull(this string text) { + if (text == null) { + return null; + } + return !string.IsNullOrWhiteSpace(text) && text.Equals("true", StringComparison.CurrentCultureIgnoreCase); + } + + /// + /// coerces a string to an int32, defaults to zero. + /// + /// The STR. + /// The default value if the string isn't a valid int. + /// + /// + /// + public static int ToInt32(this string str, int defaultValue) { + int i; + return Int32.TryParse(str, out i) ? i : defaultValue; + } + + public static bool EqualsIgnoreCase(this string str, string str2) { + if (str == null && str2 == null) { + return true; + } + + if (str == null || str2 == null) { + return false; + } + + return str.Equals(str2, StringComparison.OrdinalIgnoreCase); + } + + // ReSharper restore InconsistentNaming + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Extensions/TaskExtensions.cs b/src/Microsoft.PackageManagement/Utility/Extensions/TaskExtensions.cs new file mode 100644 index 000000000..686d43bbc --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Extensions/TaskExtensions.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Extensions { + using System.Diagnostics.CodeAnalysis; + using System.Threading.Tasks; + + internal static class TaskExtensions { + public static Task AsResultTask(this T result) { + var x = new TaskCompletionSource(TaskCreationOptions.AttachedToParent); + x.SetResult(result); + return x.Task; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Extensions/ThreadPerTaskScheduler.cs b/src/Microsoft.PackageManagement/Utility/Extensions/ThreadPerTaskScheduler.cs new file mode 100644 index 000000000..8939893c5 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Extensions/ThreadPerTaskScheduler.cs @@ -0,0 +1,44 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Extensions { + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + + internal class ThreadPerTaskScheduler : TaskScheduler { + /// Gets the tasks currently scheduled to this scheduler. + /// This will always return an empty enumerable, as tasks are launched as soon as they're queued. + protected override IEnumerable GetScheduledTasks() { + return Enumerable.Empty(); + } + + /// Starts a new thread to process the provided task. + /// The task to be executed. + protected override void QueueTask(Task task) { + new Thread(() => TryExecuteTask(task)) { + IsBackground = true + }.Start(); + } + + /// Runs the provided task on the current thread. + /// The task to be executed. + /// Ignored. + /// Whether the task could be executed on the current thread. + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { + return TryExecuteTask(task); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Extensions/Types.cs b/src/Microsoft.PackageManagement/Utility/Extensions/Types.cs new file mode 100644 index 000000000..977bfbaeb --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Extensions/Types.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Extensions { + using System; + using System.Linq; + + internal class Types { + private readonly Type _first; + private readonly Type[] _second; + + public Types(Type first, params Type[] second) { + _first = first; + _second = second; + } + + public override int GetHashCode() { + return _second.Aggregate(_first.FullName.GetHashCode(), (current, each) => current ^ each.GetHashCode()); + } + + public override bool Equals(object obj) { + if (obj == this) { + return true; + } + var other = obj as Types; + return other != null && (_first == other._first && _second.SequenceEqual(other._second)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Platform/AdminPrivilege.cs b/src/Microsoft.PackageManagement/Utility/Platform/AdminPrivilege.cs new file mode 100644 index 000000000..c65ae1af7 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Platform/AdminPrivilege.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Platform { + using System.Security.Principal; + + internal class AdminPrivilege { + /// + /// The function checks whether the current process is run as administrator. In other words, it dictates whether the + /// primary access token of the process belongs to user account that is a member of the local Administrators group and + /// it is elevated. + /// + /// + /// Returns true if the primary access token of the process belongs to user account that is a member of the local + /// Administrators group and it is elevated. Returns false if the token does not. + /// + public static bool IsElevated { + get { + var id = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(id); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Platform/AsyncProcess.cs b/src/Microsoft.PackageManagement/Utility/Platform/AsyncProcess.cs new file mode 100644 index 000000000..3728858b8 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Platform/AsyncProcess.cs @@ -0,0 +1,518 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Platform { + using System; + using System.Collections; + using System.Collections.Generic; + using System.ComponentModel; + using System.Diagnostics; + using System.Linq; + using System.Threading; + using Collections; + using Extensions; + + /// + /// This is a wrapper around the Process class to provide easy access to asynchronous + /// stdout/stderr streams. + /// TODO: add support for a cancellation token to kill the process when cancelled. + /// + internal class AsyncProcess : IDisposable { + protected Process _process; + private BlockingCollection _stdError = new BlockingCollection(); + private ManualResetEvent _stdErrStarted = new ManualResetEvent(false); + private BlockingCollection _stdOut = new BlockingCollection(); + private ManualResetEvent _stdOutStarted = new ManualResetEvent(false); + + protected AsyncProcess(Process process) { + _process = process; + } + + public IEnumerable StandardError { + get { + _stdErrStarted.WaitOne(); + return _stdError; + } + } + + public IEnumerable StandardOutput { + get { + _stdOutStarted.WaitOne(); + return _stdOut; + } + } + + public int ExitCode { + get { + return _process.ExitCode; + } + } + + public bool HasExited { + get { + return _process.HasExited; + } + } + + public DateTime ExitTime { + get { + return _process.ExitTime; + } + } + +#if !CORECLR + public IntPtr Handle { + get { + return _process.Handle; + } + } + + public int HandleCount { + get { + return _process.HandleCount; + } + } + + public IntPtr MainWindowHandle + { + get + { + return _process.MainWindowHandle; + } + } + + public string MainWindowTitle + { + get + { + return _process.MainWindowTitle; + } + } + + public ISynchronizeInvoke SynchronizingObject + { + get + { + return _process.SynchronizingObject; + } + } + + public bool Responding + { + get + { + return _process.Responding; + } + } + + public bool WaitForInputIdle(int milliseconds) + { + return _process.WaitForInputIdle(milliseconds); + } + + public bool WaitForInputIdle() + { + return WaitForInputIdle(-1); + } + + public bool CloseMainWindow() + { + return _process.CloseMainWindow(); + } + +#endif + + public void Close() + { +#if !CORECLR + _process.Close(); +#else + Dispose(); +#endif + } + + public int Id { + get { + return _process.Id; + } + } + + public string MachineName { + get { + return _process.MachineName; + } + } + + + public ProcessModule MainModule { + get { + return _process.MainModule; + } + } + + public IntPtr MaxWorkingSet { + get { + return _process.MaxWorkingSet; + } + } + + public IntPtr MinWorkingSet { + get { + return _process.MinWorkingSet; + } + } + + public ProcessModuleCollection Modules { + get { + return _process.Modules; + } + } + + public long NonpagedSystemMemorySize64 { + get { + return _process.NonpagedSystemMemorySize64; + } + } + + public long PagedMemorySize64 { + get { + return _process.PagedMemorySize64; + } + } + + public long PagedSystemMemorySize64 { + get { + return _process.PagedSystemMemorySize64; + } + } + + public long PeakPagedMemorySize64 { + get { + return _process.PeakPagedMemorySize64; + } + } + + public long PeakWorkingSet64 { + get { + return _process.PeakWorkingSet64; + } + } + + public long PeakVirtualMemorySize64 { + get { + return _process.PeakVirtualMemorySize64; + } + } + + public bool PriorityBoostEnabled { + get { + return _process.PriorityBoostEnabled; + } + set { + _process.PriorityBoostEnabled = value; + } + } + + public ProcessPriorityClass PriorityClass { + get { + return _process.PriorityClass; + } + set { + _process.PriorityClass = value; + } + } + + public long PrivateMemorySize64 { + get { + return _process.PrivateMemorySize64; + } + } + + public TimeSpan PrivilegedProcessorTime { + get { + return _process.PrivilegedProcessorTime; + } + } + + public string ProcessName { + get { + return _process.ProcessName; + } + } + + public IntPtr ProcessorAffinity { + get { + return _process.ProcessorAffinity; + } + set { + _process.ProcessorAffinity = value; + } + } + + public int SessionId { + get { + return _process.SessionId; + } + } + + public DateTime StartTime { + get { + return _process.StartTime; + } + } + + public ProcessThreadCollection Threads { + get { + return _process.Threads; + } + } + + public TimeSpan TotalProcessorTime { + get { + return _process.TotalProcessorTime; + } + } + + public TimeSpan UserProcessorTime { + get { + return _process.UserProcessorTime; + } + } + + public long VirtualMemorySize64 { + get { + return _process.VirtualMemorySize64; + } + } + + public bool EnableRaisingEvents { + get { + return _process.EnableRaisingEvents; + } + set { + _process.EnableRaisingEvents = value; + } + } + + public long WorkingSet64 { + get { + return _process.WorkingSet64; + } + } + + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + public static AsyncProcess Start(ProcessStartInfo startInfo) { + return Start(startInfo, null); + } + + private void ErrorDataReceived(object sender, DataReceivedEventArgs args) { + _stdErrStarted.Set(); + if (_stdError != null) { + _stdError.Add(args.Data ?? string.Empty); + } else { + Console.WriteLine("Attempting to add when collection is null!!!!! (stdErr)"); + _process.ErrorDataReceived -= ErrorDataReceived; + } + } + + private void OutputDataReceived(object sender, DataReceivedEventArgs args) { + _stdOutStarted.Set(); + if (_stdOut != null) { + _stdOut.Add(args.Data ?? string.Empty); + } else { + Console.WriteLine("Attempting to add when collection is null!!!!! (stdOut)"); + _process.OutputDataReceived -= OutputDataReceived; + } + } + + private void ProcessExited(object sender, EventArgs args) { + WaitForExit(); + _stdError.CompleteAdding(); + _stdOut.CompleteAdding(); + _stdErrStarted.Set(); + _stdOutStarted.Set(); + } + + public static AsyncProcess Start(ProcessStartInfo startInfo, IDictionary environment) { + startInfo.RedirectStandardError = true; + startInfo.RedirectStandardOutput = true; + startInfo.UseShellExecute = false; + var redirecting = true; + +#if !CORECLR + if (AdminPrivilege.IsElevated) { + startInfo.Verb = ""; + } else { + if ("runas".EqualsIgnoreCase(startInfo.Verb)) { + redirecting = false; + startInfo.UseShellExecute = true; + startInfo.RedirectStandardError = false; + startInfo.RedirectStandardOutput = false; + } + } +#endif + + if (environment != null) { + foreach (var i in environment.Keys) { +#if !CORECLR + startInfo.EnvironmentVariables[(string)i] = (string)environment[i]; +#else + startInfo.Environment[(string)i] = (string)environment[i]; +#endif + } + } + + var result = new AsyncProcess(new Process { + StartInfo = startInfo + }); + + result._process.EnableRaisingEvents = true; + + // set up std* access + if (redirecting) { + result._process.ErrorDataReceived += result.ErrorDataReceived; + result._process.OutputDataReceived += result.OutputDataReceived; + } + result._process.Exited += result.ProcessExited; + result._process.Start(); + + if (redirecting) { + result._process.BeginErrorReadLine(); + result._process.BeginOutputReadLine(); + } + return result; + } + + public static AsyncProcess Start(string fileName) { + return Start(new ProcessStartInfo { + FileName = fileName + }); + } + + public static AsyncProcess Start(string fileName, IDictionary environment) { + return Start(new ProcessStartInfo { + FileName = fileName + }, environment); + } + + public static AsyncProcess Start(string fileName, string parameters, IDictionary environment) { + return Start(new ProcessStartInfo { + FileName = fileName, + Arguments = parameters + }, environment); + } + + public static AsyncProcess Start(string fileName, string parameters) { + return Start(new ProcessStartInfo { + FileName = fileName, + Arguments = parameters + }); + } + + public void Kill() { + _process.Kill(); + } + + public bool WaitForExit(int milliseconds) { + return _process.WaitForExit(milliseconds); + } + + public void WaitForExit() { + WaitForExit(-1); + } + + public event EventHandler Exited { + add { + _process.Exited += value; + } + remove { + _process.Exited -= value; + } + } + + protected virtual void Dispose(bool disposing) { + if (disposing) { + if (_process != null) { + if (!_process.HasExited) { + // this object is being disposed, yet the process isn't finished. + Console.WriteLine("Killing AsyncProcess"); + _process.Kill(); + } + ((IDisposable)_process).Dispose(); + } + _process = null; + if (_stdOut != null) { + ((IDisposable)_stdOut).Dispose(); + } + _stdOut = null; + if (_stdError != null) { + ((IDisposable)_stdError).Dispose(); + } + _stdError = null; + if (_stdOutStarted != null) { + _stdOutStarted.Dispose(); + } + _stdOutStarted = null; + if (_stdErrStarted != null) { + _stdErrStarted.Dispose(); + } + _stdErrStarted = null; + } + } + + public static void EnterDebugMode() { + Process.EnterDebugMode(); + } + + public static void LeaveDebugMode() { + Process.LeaveDebugMode(); + } + + public static AsyncProcess GetProcessById(int processId, string machineName) { + return new AsyncProcess(Process.GetProcessById(processId, machineName)); + } + + public static AsyncProcess GetProcessById(int processId) { + return new AsyncProcess(Process.GetProcessById(processId)); + } + + public static AsyncProcess[] GetProcessesByName(string processName) { + return Process.GetProcessesByName(processName).Select(each => new AsyncProcess(each)).ToArray(); + } + + public static AsyncProcess[] GetProcessesByName(string processName, string machineName) { + return Process.GetProcessesByName(processName, machineName).Select(each => new AsyncProcess(each)).ToArray(); + } + + public static AsyncProcess[] GetProcesses() { + return Process.GetProcesses().Select(each => new AsyncProcess(each)).ToArray(); + } + + public static AsyncProcess[] GetProcesses(string machineName) { + return Process.GetProcesses(machineName).Select(each => new AsyncProcess(each)).ToArray(); + } + + public static AsyncProcess GetCurrentProcess() { + return new AsyncProcess(Process.GetCurrentProcess()); + } + + public void Refresh() { + _process.Refresh(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Platform/Manifest.cs b/src/Microsoft.PackageManagement/Utility/Platform/Manifest.cs new file mode 100644 index 000000000..3edf59e9e --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Platform/Manifest.cs @@ -0,0 +1,72 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Platform { + using System; + using System.Collections.Generic; + using System.Runtime.InteropServices; + using System.Text; + using System.Xml.Linq; + + internal static class Manifest { + private static readonly byte[] _utf = {0xef, 0xbb, 0xbf}; + + public static IEnumerable LoadFrom(string filename) { + var manifests = new List(); + + using (DisposableModule dll = NativeMethods.LoadLibraryEx(filename, Unused.Nothing, LoadLibraryFlags.AsImageResource | LoadLibraryFlags.AsDatafile)) { + // if we get back a valid module handle + if (!dll.IsInvalid) { + // search all the 'manifest' resources + if (NativeMethods.EnumResourceNamesEx(dll, ResourceType.Manifest, (m, type, id, param) => { + // for each manifest, check the language + NativeMethods.EnumResourceLanguagesEx(m, type, id, (m1, resourceType, resourceId, language, unused) => { + // find the specific resource + var resource = NativeMethods.FindResourceEx(m1, resourceType, resourceId, language); + if (!resource.IsInvalid) { + // get a handle to the resource data + var resourceData = NativeMethods.LoadResource(m1, resource); + if (!resourceData.IsInvalid) { + // copy the resource text out of the resource data + try { + var dataSize = NativeMethods.SizeofResource(m1, resource); + var dataPointer = NativeMethods.LockResource(resourceData); + + // make sure that the pointer and size are legit. + if (dataSize > 0 && dataPointer != IntPtr.Zero) { + var data = new byte[dataSize]; + Marshal.Copy(dataPointer, data, 0, data.Length); + var bomPresent = (data.Length >= 3 && data[0] == _utf[0] && data[1] == _utf[1] && data[2] == _utf[2]); + + // create an XElement for the data returned. + // IIRC, manifests are always UTF-8, n'est-ce pas? + manifests.Add(XElement.Parse(Encoding.UTF8.GetString(data, bomPresent ? 3 : 0, bomPresent ? data.Length - 3 : data.Length))); + } + } catch { + // skip it if it doesn't load. + } + } + } + return true; + }, Unused.Nothing, ResourceEnumFlags.None, LanguageId.None); + + return true; + }, Unused.Nothing, ResourceEnumFlags.None, 0)) { + } + } + } + return manifests; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Platform/MoveFileFlags.cs b/src/Microsoft.PackageManagement/Utility/Platform/MoveFileFlags.cs new file mode 100644 index 000000000..7da1a86cb --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Platform/MoveFileFlags.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Platform { + using System; + + [Flags] + internal enum MoveFileFlags { + ReplaceExisting = 1, + CopyAllowed = 2, + DelayUntilReboot = 4, + WriteThrough = 8 + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Platform/NativeMethods.cs b/src/Microsoft.PackageManagement/Utility/Platform/NativeMethods.cs new file mode 100644 index 000000000..f1223a519 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Platform/NativeMethods.cs @@ -0,0 +1,337 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Platform { + using System; + using System.Diagnostics.CodeAnalysis; + using System.Runtime.InteropServices; + using System.Text; + + internal enum WinVerifyTrustResult : uint { + Success = 0, + ProviderUnknown = 0x800b0001, // The trust provider is not recognized on this system + ActionUnknown = 0x800b0002, // The trust provider does not support the specified action + SubjectFormUnknown = 0x800b0003, // The trust provider does not support the form specified for the subject + SubjectNotTrusted = 0x800b0004, // The subject failed the specified verification action + UntrustedRootCert = 0x800B0109 //A certificate chain processed, but terminated in a root certificate which is not trusted by the trust provider. + } + + [Flags] + internal enum LoadLibraryFlags : uint { + DontResolveDllReferences = 0x00000001, + AsDatafile = 0x00000002, + LoadWithAlteredSearchPath = 0x00000008, + LoadIgnoreCodeAuthzLevel = 0x00000010, + AsImageResource = 0x00000020, + } + + [Flags] + internal enum ResourceEnumFlags : uint { + None = 0x00000000, + LanguageNeutral = 0x00000001, + Mui = 0x00000002, + Validate = 0x00000008, + } + + public class DisposableModule : IDisposable { + private Module _module; + + public bool IsInvalid { + get { + return _module.IsInvalid; + } + } + + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void Dispose(bool disposing) { + if (disposing) { + _module.Free(); + } + } + + [SuppressMessage("Microsoft.Usage", "CA2225:OperatorOverloadsHaveNamedAlternates", Justification = "There is no need for such.")] + public static implicit operator Module(DisposableModule instance) { + return instance._module; + } + + [SuppressMessage("Microsoft.Usage", "CA2225:OperatorOverloadsHaveNamedAlternates", Justification = "There is no need for such.")] + public static implicit operator DisposableModule(Module module) { + return new DisposableModule { + _module = module + }; + } + } + + [StructLayout(LayoutKind.Explicit)] + public struct Module { + [FieldOffset(0)] + public IntPtr handle; + + public Module(IntPtr ptr) { + handle = ptr; + } + + public bool IsInvalid { + get { + return handle == IntPtr.Zero; + } + } + + public void Free() { + if (!IsInvalid) { + NativeMethods.FreeLibrary(this); + } + + handle = IntPtr.Zero; + } + } + + [StructLayout(LayoutKind.Explicit)] + public struct ResourceType { + public static ResourceType None = new ResourceType(0); + public static ResourceType Cursor = new ResourceType(1); + public static ResourceType Bitmap = new ResourceType(2); + public static ResourceType Icon = new ResourceType(3); + public static ResourceType Menu = new ResourceType(4); + public static ResourceType Dialog = new ResourceType(5); + public static ResourceType String = new ResourceType(6); + public static ResourceType FontDir = new ResourceType(7); + public static ResourceType Font = new ResourceType(8); + public static ResourceType Accelerator = new ResourceType(9); + public static ResourceType RCData = new ResourceType(10); + public static ResourceType MessageTable = new ResourceType(11); + public static ResourceType GroupCursor = new ResourceType(12); + public static ResourceType GroupIcon = new ResourceType(14); + public static ResourceType Version = new ResourceType(16); + public static ResourceType DialogInclud = new ResourceType(17); + public static ResourceType PlugPlay = new ResourceType(19); + public static ResourceType Vxd = new ResourceType(20); + public static ResourceType AniCursor = new ResourceType(21); + public static ResourceType AniIcon = new ResourceType(22); + public static ResourceType Html = new ResourceType(23); + public static ResourceType Manifest = new ResourceType(24); + + [FieldOffset(0)] + public IntPtr handle; + + public ResourceType(IntPtr ptr) { + handle = ptr; + } + + public ResourceType(int ptr) { + handle = (IntPtr)ptr; + } + + public bool IsInvalid { + get { + return handle == IntPtr.Zero; + } + } + } + + [StructLayout(LayoutKind.Explicit)] + public struct ResourceId { + [FieldOffset(0)] + public IntPtr handle; + + public ResourceId(IntPtr ptr) { + handle = ptr; + } + + public bool IsInvalid { + get { + return handle == IntPtr.Zero; + } + } + } + + [StructLayout(LayoutKind.Explicit)] + public struct Resource { + [FieldOffset(0)] + public IntPtr handle; + + public Resource(IntPtr ptr) { + handle = ptr; + } + + public bool IsInvalid { + get { + return handle == IntPtr.Zero; + } + } + } + + [StructLayout(LayoutKind.Explicit)] + public struct ResourceData { + [FieldOffset(0)] + public IntPtr handle; + + public ResourceData(IntPtr ptr) { + handle = ptr; + } + + public bool IsInvalid { + get { + return handle == IntPtr.Zero; + } + } + } + + [StructLayout(LayoutKind.Explicit)] + public struct Unused { + internal static Unused Nothing; + + [FieldOffset(0)] + public IntPtr handle; + + public Unused(IntPtr ptr) { + handle = ptr; + } + + public bool IsInvalid { + get { + return handle == IntPtr.Zero; + } + } + } + + [StructLayout(LayoutKind.Explicit)] + internal struct LanguageId { + internal static LanguageId None; + + [FieldOffset(0)] + private UInt16 value; + } + + internal delegate bool EnumResourceTypes([MarshalAs(UnmanagedType.SysInt)] Module module, ResourceType type, Unused unused); + + internal delegate bool EnumResourceNames(Module module, ResourceType type, ResourceId resourceId, Unused unused); + + internal delegate bool EnumResourceLanguages(Module module, ResourceType type, ResourceId resourceId, LanguageId language, Unused unused); + + internal static class NativeMethods { + [DllImport("mpr.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern int WNetGetConnection([MarshalAs(UnmanagedType.LPTStr)] string localName, [MarshalAs(UnmanagedType.LPTStr)] StringBuilder remoteName, ref int length); + + [DllImport("user32")] + internal static extern int LoadString(Module module, uint stringId, StringBuilder buffer, int bufferSize); + + [DllImport("wintrust.dll", ExactSpelling = true, SetLastError = false, CharSet = CharSet.Unicode)] + internal static extern WinVerifyTrustResult WinVerifyTrust(IntPtr hwnd, [MarshalAs(UnmanagedType.LPStruct)] Guid pgActionID, WinTrustData pWVTData); + +#if !CORECLR + /// + /// Loads the specified module into the address space of the calling process. + /// + /// The name of the module. + /// This parameter is reserved for future use. + /// The action to be taken when loading the module. + /// + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern Module LoadLibraryEx(string filename, Unused unused, LoadLibraryFlags dwFlags); + + [DllImport("kernel32.dll", EntryPoint = "MoveFileEx", CharSet = CharSet.Unicode)] + internal static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, MoveFileFlags dwFlags); + + /// + /// Enumerates resource types within a binary. + /// + /// Handle to a module to search. + /// Pointer to the function to be called for each resource type. + /// Value passed to the callback function. + /// The type of file to be searched. + /// Language ID + /// Returns TRUE if successful, otherwise, FALSE. + [DllImport("kernel32.dll", EntryPoint = "EnumResourceTypesExW", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern bool EnumResourceTypesEx(Module module, EnumResourceTypes callback, Unused unused, ResourceEnumFlags flags, LanguageId langid); + + [DllImport("kernel32.dll", EntryPoint = "EnumResourceNamesExW", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern bool EnumResourceNamesEx(Module module, ResourceType resourceType, EnumResourceNames callback, Unused unused, ResourceEnumFlags flags, uint langid); + + [DllImport("kernel32.dll", EntryPoint = "EnumResourceLanguagesExW", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern bool EnumResourceLanguagesEx(Module module, ResourceType resourceType, ResourceId resourceId, EnumResourceLanguages callback, Unused unused, ResourceEnumFlags flags, LanguageId language); + + [DllImport("kernel32.dll", EntryPoint = "FindResourceExW", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern Resource FindResourceEx(Module module, ResourceType resourceType, ResourceId resourceId, LanguageId language); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern ResourceData LoadResource(Module module, Resource resource); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern IntPtr LockResource(ResourceData data); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern int SizeofResource(Module module, Resource hResInfo); + + [DllImport("kernel32")] + internal static extern bool FreeLibrary(Module instance); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + public static extern void OutputDebugString(string debugMessageText); +#else + [DllImport("api-ms-win-core-file-l2-1-1.dll", EntryPoint="MoveFileEx", CharSet=CharSet.Unicode)] + internal static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, MoveFileFlags dwFlags); + /// + /// Loads the specified module into the address space of the calling process. + /// + /// The name of the module. + /// This parameter is reserved for future use. + /// The action to be taken when loading the module. + /// + + [DllImport("api-ms-win-core-libraryloader-l1-2-0.dll", EntryPoint="LoadLibraryExW", SetLastError=true, CharSet=CharSet.Unicode)] + internal static extern Module LoadLibraryEx(string filename, Unused unused, LoadLibraryFlags dwFlags); + + /// + /// Enumerates resource types within a binary. + /// + /// Handle to a module to search. + /// Pointer to the function to be called for each resource type. + /// Value passed to the callback function. + /// The type of file to be searched. + /// Language ID + /// Returns TRUE if successful, otherwise, FALSE. + [DllImport("api-ms-win-core-libraryloader-l1-2-0.dll", EntryPoint = "EnumResourceTypesExW", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern bool EnumResourceTypesEx(Module module, EnumResourceTypes callback, Unused unused, ResourceEnumFlags flags, LanguageId langid); + + [DllImport("api-ms-win-core-libraryloader-l1-2-0.dll", EntryPoint = "EnumResourceNamesExW", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern bool EnumResourceNamesEx(Module module, ResourceType resourceType, EnumResourceNames callback, Unused unused, ResourceEnumFlags flags, uint langid); + + [DllImport("api-ms-win-core-libraryloader-l1-2-0.dll", EntryPoint = "EnumResourceLanguagesExW", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern bool EnumResourceLanguagesEx(Module module, ResourceType resourceType, ResourceId resourceId, EnumResourceLanguages callback, Unused unused, ResourceEnumFlags flags, LanguageId language); + + [DllImport("api-ms-win-core-libraryloader-l1-2-0.dll", EntryPoint = "FindResourceExW", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern Resource FindResourceEx(Module module, ResourceType resourceType, ResourceId resourceId, LanguageId language); + + [DllImport("api-ms-win-core-libraryloader-l1-2-0.dll", SetLastError = true)] + internal static extern ResourceData LoadResource(Module module, Resource resource); + + [DllImport("api-ms-win-core-libraryloader-l1-2-0.dll", SetLastError = true)] + internal static extern IntPtr LockResource(ResourceData data); + + [DllImport("api-ms-win-core-libraryloader-l1-2-0.dll", SetLastError = true)] + internal static extern int SizeofResource(Module module, Resource hResInfo); + + [DllImport("api-ms-win-core-libraryloader-l1-2-0.dll")] + internal static extern bool FreeLibrary(Module instance); + + [DllImport("api-ms-win-core-debug-l1-1-1", CharSet=CharSet.Unicode, EntryPoint="OutputDebugStringW")] + public static extern void OutputDebugString(string debugMessageText); +#endif + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Platform/WinTrustData.cs b/src/Microsoft.PackageManagement/Utility/Platform/WinTrustData.cs new file mode 100644 index 000000000..3a34b4eef --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Platform/WinTrustData.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Platform { + using System; + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal class WinTrustData + { +#if !CORECLR + private UInt32 StructSize = (UInt32)Marshal.SizeOf(typeof(WinTrustData)); +#else + private UInt32 StructSize = (UInt32)Marshal.SizeOf(); +#endif + private IntPtr PolicyCallbackData = IntPtr.Zero; + private IntPtr SIPClientData = IntPtr.Zero; + private uint UIChoice = 2; + private uint RevocationChecks = 0; + private uint UnionChoice = 1; + private IntPtr FileInfoPtr; + private uint StateAction = 0; + private IntPtr StateData = IntPtr.Zero; + private String URLReference; + private uint ProvFlags = 0x00000040; // check revocation chain. + private uint UIContext = 0; + + // constructor for silent WinTrustDataChoice.File check + public WinTrustData(String filename) { + var wtfiData = new WinTrustFileInfo(filename); +#if !CORECLR + FileInfoPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(WinTrustFileInfo))); +#else + FileInfoPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf()); +#endif + Marshal.StructureToPtr(wtfiData, FileInfoPtr, false); + } + + ~WinTrustData() { + Marshal.FreeCoTaskMem(FileInfoPtr); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Platform/WinTrustFileInfo.cs b/src/Microsoft.PackageManagement/Utility/Platform/WinTrustFileInfo.cs new file mode 100644 index 000000000..4547fcce4 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Platform/WinTrustFileInfo.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Platform { + using System; + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal class WinTrustFileInfo { +#if !CORECLR + private UInt32 StructSize = (UInt32)Marshal.SizeOf(typeof(WinTrustFileInfo)); +#else + private UInt32 StructSize = (UInt32)Marshal.SizeOf(); +#endif + private IntPtr FilePath; // required, file name to be verified + private IntPtr hFile = IntPtr.Zero; // optional, open handle to FilePath + private IntPtr pgKnownSubject = IntPtr.Zero; // optional, subject type if it is known + + public WinTrustFileInfo(String filePath) { +#if CORECLR + FilePath = Marshal.StringToCoTaskMemUni(filePath); +#else + FilePath = Marshal.StringToCoTaskMemAuto(filePath); +#endif + } + + ~WinTrustFileInfo() { + Marshal.FreeCoTaskMem(FilePath); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Plugin/AssignableTypeComparer.cs b/src/Microsoft.PackageManagement/Utility/Plugin/AssignableTypeComparer.cs new file mode 100644 index 000000000..94a89fbd3 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Plugin/AssignableTypeComparer.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Plugin { + using System; + using System.Reflection; + using System.Collections.Generic; + using Extensions; + + internal class AssignableTypeComparer : IEqualityComparer { + public static readonly AssignableTypeComparer Instance = new AssignableTypeComparer(); + + public bool Equals(Type x, Type y) { + return IsAssignableOrCompatible(x, y); + } + + public int GetHashCode(Type obj) { + // unused. + return -1; + } + + public static bool IsAssignableOrCompatible(Type x, Type y) { + if (x == null) { + return y == null; + } + + return x == y || x.GetTypeInfo().IsAssignableFrom(y.GetTypeInfo()) || x.CanDynamicCastFrom(y); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Plugin/DynamicInterface.cs b/src/Microsoft.PackageManagement/Utility/Plugin/DynamicInterface.cs new file mode 100644 index 000000000..26f2f024c --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Plugin/DynamicInterface.cs @@ -0,0 +1,317 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Plugin { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using Async; + using Collections; + using Extensions; + using Api; + + internal static class DynamicInterface { + internal delegate void OnUnhandledException(string method, Exception exception); + + private static readonly Dictionary _isCreatableFromTypesCache = new Dictionary(); + private static readonly Dictionary _isCastableFromTypesCache = new Dictionary(); + + public static TInterface Create(params Type[] types) { + return (TInterface)typeof (TInterface).Create(types); + } + + public static TInterface Create(this Type type) { + return (TInterface)typeof (TInterface).Create(type); + } + + public static object Create(this Type tInterface, params Type[] types) { + if (tInterface == null) { + throw new ArgumentNullException("tInterface"); + } + + types = types ?? new Type[0]; + + if (!tInterface.GetVirtualMethods().Any()) { + throw new Exception("Interface Type '{0}' doesn not have any virtual or abstract methods".format(tInterface.FullNiceName())); + } + + if (!tInterface.CanCreateFrom(types)) { + var missing = GetMissingMethods(tInterface, types).ToArray(); + var badctors = FilterOnMissingDefaultConstructors(types).ToArray(); + + var msg = badctors.Length == 0 ? "" + : "\r\nTypes ({0}) do not support a Default Constructor\r\n".format(badctors.Select(each => each.FullName).Quote().JoinWithComma()); + + msg += missing.Length == 0 ? "" : + "\r\nTypes ({0}) are missing the following methods from interface ('{1}'):\r\n {2}".format( + types.Select(each => each.FullName).Quote().JoinWithComma(), + tInterface.FullNiceName(), + missing.Select(each => each.ToSignatureString()).Quote().JoinWith("\r\n ")); + + throw new Exception(msg); + } + + // create actual instance + return CreateProxy(tInterface, types.Select(Activator.CreateInstance).ToArray()); + } + + private static IEnumerable Flatten(IEnumerable items) { + if (items == null) { + yield break; + } + + if (items is IAsyncAction) { + yield return items; + yield break; + } + + foreach (var item in items) { + if (item is object[] || item is IEnumerable) { + foreach (var inner in Flatten(item as IEnumerable)) { + if (inner != null) { + yield return inner; + } + } + continue; + } + yield return item; + } + } + + private static IEnumerable Flatten(params object[] items) { + return Flatten(items as IEnumerable); + } + + public static TInterface DynamicCast(params object[] instances) { + return (TInterface)DynamicCast(typeof (TInterface), instances); + } + + private static object DynamicCast(Type tInterface, params object[] instances) { + if (tInterface == null) { + throw new ArgumentNullException("tInterface"); + } + + if (instances.Length == 0) { + throw new ArgumentException("No instances given", "instances"); + } + + if (instances.Length == 1) { + // shortcut for string coercion + if (tInterface == typeof (string)) { + return instances[0] == null ? null : instances[0].ToString(); + } + + var objects = instances[0] as IEnumerable; + if (objects != null) { + // if the tInterface is an IEnumerable + // then we'll just dynamic cast the items in the collection to the target type. + if (tInterface.IsIEnumerableT()) { + var elementType = tInterface.GetGenericArguments().FirstOrDefault(); + + if (elementType != null) { + return objects.Select(each => DynamicCast(elementType, each)).ToIEnumerableT(elementType); + } + } + + if (tInterface.IsArray && tInterface.GetArrayRank() == 1) { + var elementType = tInterface.GetElementType(); + if (elementType != null) { + return objects.Select(each => DynamicCast(elementType, each)).ToArrayT(elementType); + } + } + } + } + + if (!tInterface.GetVirtualMethods().Any()) { + throw new Exception("Interface Type '{0}' doesn not have any virtual or abstract methods".format(tInterface.FullNiceName())); + } + instances = Flatten(instances).ToArray(); + + if (instances.Any(each => each == null)) { + throw new ArgumentException("One or more instances are null", "instances"); + } + + // shortcut if the interface is already implemented in the object. + if (instances.Length == 1 && tInterface.IsInstanceOfType(instances[0])) { + return instances[0]; + } + + if (!tInterface.CanDynamicCastFrom(instances)) { + var missing = GetMethodsMissingFromInstances(tInterface, instances); + var msg = "\r\nObjects are missing the following methods from interface ('{0}'):\r\n {1}".format( + tInterface.FullNiceName(), + missing.Select(each => each.ToSignatureString()).Quote().JoinWith("\r\n ")); + + throw new Exception(msg); + } + + return CreateProxy(tInterface, instances); + } + + public static bool CanCreateFrom(this Type tInterface, params Type[] types) { + return _isCreatableFromTypesCache.GetOrAdd(new Types(tInterface, types), () => { + // if there isn't a default constructor, we can't use that type to create instances + return types.All(actualType => actualType.GetDefaultConstructor() != null) && CanDynamicCastFrom(tInterface, types); + }); + } + + public static bool CanCreateFrom(params Type[] types) { + return CanCreateFrom(typeof (TInterface), types); + } + + public static bool CanDynamicCastFrom(this Type tInterface, params Type[] types) { + return _isCastableFromTypesCache.GetOrAdd(new Types(tInterface, types), () => !GetMissingMethods(tInterface, types).Any()); + } + + private static IEnumerable FilterOnMissingDefaultConstructors(params Type[] types) { + return types.Where(actualType => actualType.GetDefaultConstructor() == null); + } + + private static IEnumerable GetMissingMethods(Type tInterface, params Type[] types) { + return tInterface.GetRequiredMethods().Where(method => types.GetPublicMethods().FindMethod(method) == null); + } + + public static bool CanDynamicCastFrom(this Type tInterface, params object[] instances) { + if (tInterface == null) { + throw new ArgumentNullException("tInterface"); + } + + if (instances == null) { + throw new ArgumentNullException("instances"); + } + + if (instances.Length == 0) { + throw new ArgumentException("No instances given", "instances"); + } + + if (instances.Any(each => each == null)) { + throw new ArgumentException("One or more instances are null", "instances"); + } + + // this will be faster if this type has been checked before. + if (CanCreateFrom(tInterface, instances.Select(each => each.GetType()).ToArray())) { + return true; + } + +#if DEEPDEBUG + var missing = GetMethodsMissingFromInstances(tInterface,instances).ToArray(); + + if (missing.Length > 0 ) { + var msg = "\r\nObjects are missing the following methods from interface ('{0}'):\r\n {1}".format( + tInterface.FullNiceName(), + missing.Select(each => each.ToSignatureString()).Quote().JoinWith("\r\n ")); + Debug.WriteLine(msg); + } +#endif + // see if any specified object has something for every required method. + return !instances.Aggregate((IEnumerable)tInterface.GetRequiredMethods(), GetMethodsMissingFromInstance).Any(); + } + + private static IEnumerable GetMethodsMissingFromInstances(Type tInterface, params object[] instances) { + return instances.Aggregate((IEnumerable)tInterface.GetRequiredMethods(), GetMethodsMissingFromInstance); + } + + private static IEnumerable GetMethodsMissingFromInstance(IEnumerable methods, object instance) { + var instanceSupportsMethod = DynamicInterfaceExtensions.GenerateInstanceSupportsMethod(instance); + var instanceType = instance.GetType(); + + var instanceMethods = instanceType.GetPublicMethods(); + var instanceFields = instanceType.GetPublicDelegateFields(); + var instanceProperties = instanceType.GetPublicDelegateProperties(); + + // later, we can check for a delegate-creation function that can deliver a delegate to us by name and parameter types. + // currently, we don't need that, so we're not going to implement it right away. + + return methods.Where(method => + !instanceSupportsMethod(method.Name) || ( + instanceMethods.FindMethod(method) == null && + instanceFields.FindDelegate(instance, method) == null && + instanceProperties.FindDelegate(instance, method) == null + )); + } + + private static object CreateProxy(Type tInterface, params object[] instances) { + var matrix = instances.SelectMany(instance => { + var instanceType = instance.GetType(); + // get all the public interfaces for the instances and place them at the top + // and let it try to bind against those. + return instanceType.GetInterfaces().Where(each => each.GetTypeInfo().IsPublic).Select(each => new { + instance, + SupportsMethod = DynamicInterfaceExtensions.GenerateInstanceSupportsMethod(instance), + Type = each, + Methods = each.GetPublicMethods(), + Fields = each.GetPublicDelegateFields(), + Properties = each.GetPublicDelegateProperties() + }).ConcatSingleItem(new { + instance, + SupportsMethod = DynamicInterfaceExtensions.GenerateInstanceSupportsMethod(instance), + Type = instanceType, + Methods = instanceType.GetPublicMethods(), + Fields = instanceType.GetPublicDelegateFields(), + Properties = instanceType.GetPublicDelegateProperties() + }); + }).ToArray(); + + var instanceMethods = new OrderedDictionary>(); + var delegateMethods = new List(); + var stubMethods = new List(); + var usedInstances = new List(); + + foreach (var method in tInterface.GetVirtualMethods()) { + // figure out where it's going to get implemented + var found = false; + foreach (var instance in matrix) { + if (method.Name == "IsMethodImplemented") { + // skip for now, we'll implement this at the end + found = true; + break; + } + + if (instance.SupportsMethod(method.Name)) { + var instanceMethod = instance.Methods.FindMethod(method); + if (instanceMethod != null) { + instanceMethods.GetOrAdd(instance.Type, () => new List()).Add(method, instanceMethod); + if (!usedInstances.Contains(instance.Type, instance.instance)) { + usedInstances.Add(instance.Type, instance.instance); + } + found = true; + break; + } + + var instanceDelegate = instance.Fields.FindDelegate(instance.instance, method) ?? instance.Properties.FindDelegate(instance.instance, method); + if (instanceDelegate != null) { + delegateMethods.Add(instanceDelegate, method); + found = true; + break; + } + } + } + if (!found && (tInterface.GetTypeInfo().IsInterface || method.IsAbstract)) { +#if xDEEPDEBUG + Console.WriteLine(" Generating stub method for {0} -> {1}".format(tInterface.NiceName(), method.ToSignatureString())); +#endif + stubMethods.Add(method); + } + } + + return DynamicType.Create(tInterface, instanceMethods, delegateMethods, stubMethods, usedInstances); + } + + public static IEnumerable FindCompatibleTypes(this Assembly assembly) { + return assembly == null ? Enumerable.Empty() : assembly.CreatableTypes().Where(each => CanCreateFrom(each)); + } + + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Plugin/DynamicInterfaceExtensions.cs b/src/Microsoft.PackageManagement/Utility/Plugin/DynamicInterfaceExtensions.cs new file mode 100644 index 000000000..38b2bc960 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Plugin/DynamicInterfaceExtensions.cs @@ -0,0 +1,352 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Plugin +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using Collections; + using Extensions; + + public static class DynamicInterfaceExtensions + { + private static readonly Type[] _emptyTypes = { }; + private static readonly Dictionary _methodCache = new Dictionary(); + private static readonly Dictionary _methodCacheForTypes = new Dictionary(); + private static readonly Dictionary _delegateFieldsCache = new Dictionary(); + private static readonly Dictionary _delegatePropertiesCache = new Dictionary(); + private static readonly Dictionary _requiredMethodsCache = new Dictionary(); + private static readonly Dictionary _virtualMethodsCache = new Dictionary(); + private static readonly Dictionary _creatableTypesCache = new Dictionary(); + + public static MethodInfo FindMethod(this MethodInfo[] methods, MethodInfo methodSignature) + { + // this currently returns the first thing that matches acceptably. + + // we'd really like to find the *best* match, but still be able to have the earlier ones override the later ones. + // which is + + return methods.FirstOrDefault(candidate => DoNamesMatchAcceptably(methodSignature.Name, candidate.Name) && DoSignaturesMatchAcceptably(methodSignature, candidate)); + } + + public static MethodInfo FindMethod(this MethodInfo[] methods, Type delegateType) + { + return methods.FirstOrDefault(candidate => DoNamesMatchAcceptably(delegateType.Name, candidate.Name) && delegateType.IsDelegateAssignableFromMethod(candidate)); + } + + public static Delegate FindDelegate(this FieldInfo[] fields, object actualInstance, MethodInfo signature) + { + return (from field in fields + let value = field.GetValue(actualInstance) as Delegate + where DoNamesMatchAcceptably(signature.Name, field.Name) && field.FieldType.IsDelegateAssignableFromMethod(signature) && value != null + select value).FirstOrDefault(); + } + + public static Delegate FindDelegate(this PropertyInfo[] properties, object actualInstance, MethodInfo signature) + { + return (from property in properties + let value = property.GetValue(actualInstance, null) as Delegate + where DoNamesMatchAcceptably(signature.Name, property.Name) && property.PropertyType.IsDelegateAssignableFromMethod(signature) && value != null + select value).FirstOrDefault(); + } + + public static Delegate FindDelegate(this FieldInfo[] fields, object actualInstance, Type delegateType) + { + return (from candidate in fields + let value = candidate.GetValue(actualInstance) as Delegate + where value != null && DoNamesMatchAcceptably(delegateType.Name, candidate.Name) && delegateType.IsDelegateAssignableFromDelegate(value.GetType()) + select value).FirstOrDefault(); + } + + public static Delegate FindDelegate(this PropertyInfo[] properties, object actualInstance, Type delegateType) + { + return (from candidate in properties + let value = candidate.GetValue(actualInstance, null) as Delegate + where value != null && DoNamesMatchAcceptably(delegateType.Name, candidate.Name) && delegateType.IsDelegateAssignableFromDelegate(value.GetType()) + select value).FirstOrDefault(); + } + + private static bool DoNamesMatchAcceptably(string originalName, string candidateName) + { + if (string.IsNullOrWhiteSpace(originalName) || string.IsNullOrWhiteSpace(candidateName) || originalName[0] == '_' || candidateName[0] == '_') + { + // names that start with underscores are considered to be private and not supported. + return false; + } + + if (originalName.EqualsIgnoreCase(candidateName)) + { + return true; + } + + // transform non-leading underscores to nothing. + candidateName = candidateName.Replace("_", ""); + originalName = originalName.Replace("_", ""); + + // this allows GetSomething to be the same as Get_Some_thing() or get_Something ... + if (originalName.EqualsIgnoreCase(candidateName)) + { + return true; + } + + return false; + } + + private static bool DoSignaturesMatchAcceptably(MethodInfo member, MethodInfo candidate) + { + return candidate.GetParameterTypes().SequenceEqual(member.GetParameterTypes(), AssignableTypeComparer.Instance) && (AssignableTypeComparer.IsAssignableOrCompatible(member.ReturnType, candidate.ReturnType) || member.ReturnType == typeof(void)); + } + + internal static MethodInfo[] GetPublicMethods(this Type type) + { + return _methodCache.GetOrAdd(type, () => type != null ? type.GetMethods(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance) : new MethodInfo[0]); + } + + internal static MethodInfo[] GetPublicMethods(this Type[] types) + { + return _methodCacheForTypes.GetOrAdd(types, () => types.SelectMany(each => each.GetPublicMethods()).ToArray()); + } + + internal static IEnumerable GetPublicFields(this Type type) + { + return type != null ? type.GetFields(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public) : Enumerable.Empty(); + } + + internal static FieldInfo[] GetPublicDelegateFields(this Type type) + { + return _delegateFieldsCache.GetOrAdd(type, () => type.GetPublicFields().Where(each => each.FieldType.GetTypeInfo().BaseType == typeof(MulticastDelegate)).ToArray()); + } + + internal static PropertyInfo[] GetPublicDelegateProperties(this Type type) + { + return _delegatePropertiesCache.GetOrAdd(type, () => type.GetPublicProperties().Where(each => each.PropertyType.GetTypeInfo().BaseType == typeof(MulticastDelegate)).ToArray()); + } + + internal static IEnumerable GetPublicProperties(this Type type) + { + return type != null ? type.GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.Public) : Enumerable.Empty(); + } + + private static IEnumerable DisambiguateMethodsBySignature(params IEnumerable[] setsOfMethods) + { + var unique = new HashSet(); + + return setsOfMethods.SelectMany(methodSet => methodSet.ReEnumerable()).Where(method => + { + var sig = method.ToSignatureString(); + if (!unique.Contains(sig)) + { + unique.Add(sig); + return true; + } + return false; + }); + } + + internal static MethodInfo[] GetVirtualMethods(this Type type) + { + return _virtualMethodsCache.GetOrAdd(type, () => + { + var methods = (type.GetTypeInfo().IsInterface + ? (IEnumerable)type.GetMethods(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance) + : (IEnumerable)type.GetMethods(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance).Where(each => each.IsAbstract || each.IsVirtual)); + + methods = methods.Where(each => each.Name != "Dispose"); + + // option 1: + // if the target type is a class, and implements an interface -- and the implementation of that interface is already present (ie, abstract class Foo : IDisposable { public void Dispose() {} } ) then + // the generated type should not try to create a method for that interface + + // option 2: + // I think we're just talking about IDisposable here. maybe we shouldn't try to ducktype IDisposable at all. + + // try option2 : +#if DEEP_DEBUG + var ifaces = type.GetInterfaces().ToArray(); + if (ifaces.Any()) { + Console.WriteLine("Interface: {0}",ifaces.Select(each=>each.Name).JoinWithComma()); + if (ifaces.Any(each => each == typeof (IDisposable))) { + Debugger.Break(); + } + } +#endif + + var interfaceMethods = type.GetInterfaces().Where(each => each != typeof(IDisposable)).SelectMany(GetVirtualMethods); + + return DisambiguateMethodsBySignature(methods, interfaceMethods).ToArray(); + }); + } + + internal static MethodInfo[] GetRequiredMethods(this Type type) + { + return _requiredMethodsCache.GetOrAdd(type, () => + { + var i = type.GetVirtualMethods().Where(each => each.GetCustomAttributes(true).Any(attr => attr.GetType().Name.Equals("RequiredAttribute", StringComparison.OrdinalIgnoreCase))).ToArray(); + return i; + }); + } + + internal static ConstructorInfo GetDefaultConstructor(this Type t) + { + try + { + return t.GetConstructor(_emptyTypes); + } + catch + { + } + return null; + } + + internal static string ToSignatureString(this MethodInfo method) + { + return "{0} {1}({2})".format(method.ReturnType.Name, method.Name, method.GetParameters().Select(each => "{0} {1}".format(each.ParameterType.NiceName(), each.Name)).JoinWithComma()); + } + + public static string NiceName(this Type type) + { + if (!type.GetTypeInfo().IsGenericType) + { + return type.Name; + } + var typeName = type.GetGenericTypeDefinition().Name; + typeName = typeName.Substring(0, typeName.IndexOf('`')); + return typeName + "<" + string.Join(",", type.GetGenericArguments().Select(NiceName).ToArray()) + ">"; + } + + public static string FullNiceName(this Type type) + { + if (!type.GetTypeInfo().IsGenericType) + { + return type.FullName; + } + var typeName = type.GetGenericTypeDefinition().FullName; + typeName = typeName.Substring(0, typeName.IndexOf('`')); + return typeName + "<" + string.Join(",", type.GetGenericArguments().Select(NiceName).ToArray()) + ">"; + } + + internal static Func GenerateInstancesSupportsMethod(object[] actualInstance) + { + var ism = actualInstance.Select(GenerateInstanceSupportsMethod).ToArray(); + return (s) => ism.Any(each => each(s)); + } + + internal static Func GenerateInstanceSupportsMethod(object actualInstance) + { + // if the object implements an IsMethodImplemented Method, we'll be using that + // to see if the method is actually supposed to be used. + // this enables an implementor to physically implement the function in the class + // yet treat it as if it didn't. (see the PowerShellPackageProvider) + var imiMethodInfo = actualInstance.GetType().GetMethod("IsMethodImplemented", new[] { + typeof (string) + }); + return imiMethodInfo == null ? (s) => true : actualInstance.CreateProxiedDelegate>(imiMethodInfo); + } + + /// + /// This extension uses the DuckTyper to transform an object into a given interface or type. + /// + /// + /// + /// + public static TInterface As(this object instance) + { + if (typeof(TInterface).IsDelegate()) + { + // find a function in this object that matches the delegate that we are given + // and return that instead. + if (instance.GetType().IsDelegate()) + { + if (typeof(TInterface).IsDelegateAssignableFromDelegate(instance.GetType())) + { + return ((Delegate)instance).CreateProxiedDelegate(); + } + throw new Exception("Delegate '{0}' can not be created from Delegate '{1}'.".format(typeof(TInterface).NiceName(), instance.GetType().NiceName())); + } + + var instanceSupportsMethod = GenerateInstanceSupportsMethod(instance); + var instanceType = instance.GetType(); + + var instanceMethods = instanceType.GetPublicMethods(); + var instanceFields = instanceType.GetPublicDelegateFields(); + var instanceProperties = instanceType.GetPublicDelegateProperties(); + + if (!instanceSupportsMethod(typeof(TInterface).Name)) + { + throw new Exception("Generation of Delegate '{0}' not supported from object.".format(typeof(TInterface).NiceName())); + } + + var method = instanceMethods.FindMethod(typeof(TInterface)); + if (method != null) + { + return instance.CreateProxiedDelegate(method); + } + var instanceDelegate = instanceFields.FindDelegate(instance, typeof(TInterface)) ?? instanceProperties.FindDelegate(instance, typeof(TInterface)); + if (instanceDelegate != null) + { + if (instanceDelegate is TInterface) + { + return (TInterface)(object)instanceDelegate; + } + return instanceDelegate.CreateProxiedDelegate(); + } + return (TInterface)(object)typeof(TInterface).CreateEmptyDelegate(); + // throw new Exception("Delegate '{0}' not matched in object.".format(typeof (TInterface).NiceName())); + } + return DynamicInterface.DynamicCast(instance); + } + + public static TInterface Extend(this object obj, params object[] objects) + { + return DynamicInterface.DynamicCast(objects, obj); + } + + public static bool IsDelegate(this Type t) + { + return t.GetTypeInfo().BaseType == typeof(MulticastDelegate); + } + + public static bool IsIEnumerableT(this Type t) + { +#if FRAMEWORKv45 + return t.IsConstructedGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>); +#else + try { + return t.GetGenericTypeDefinition() == typeof (IEnumerable<>); + } catch { + } + return false; +#endif + } + + public static IEnumerable CreatableTypes(this Assembly assembly) + { +#if DEEPDEBUG + var x = _creatableTypesCache.GetOrAdd(assembly, () => assembly.GetTypes().Where(each => each.GetTypeInfo().GetTypeInfo().IsPublic && !each.GetTypeInfo().IsEnum && !each.GetTypeInfo().IsInterface && !each.GetTypeInfo().IsAbstract && each.GetDefaultConstructor() != null && each.GetTypeInfo().BaseType != typeof(MulticastDelegate)).ToArray()); + foreach (var i in x) { + Debug.WriteLine("Creatable Type in assembly {0} - {1}", assembly.GetName(), i.Name); + } +#endif +#if CORECLR + return _creatableTypesCache.GetOrAdd(assembly, + () => assembly.DefinedTypes.Where(each => each.IsPublic && !each.IsEnum && !each.IsInterface && !each.IsAbstract && each.AsType().GetDefaultConstructor() != null && each.BaseType != typeof (MulticastDelegate)).Select(item => item.AsType()).ToArray()); +#else + return _creatableTypesCache.GetOrAdd(assembly, + () => assembly.GetTypes().Where(each => each.IsPublic && !each.IsEnum && !each.IsInterface && !each.IsAbstract && each.GetDefaultConstructor() != null && each.BaseType != typeof (MulticastDelegate)).ToArray()); +#endif + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Plugin/DynamicType.cs b/src/Microsoft.PackageManagement/Utility/Plugin/DynamicType.cs new file mode 100644 index 000000000..368765757 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Plugin/DynamicType.cs @@ -0,0 +1,195 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Plugin { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using System.Reflection.Emit; + using Collections; + using Extensions; + + internal class DynamicType { + private static int _typeCounter = 1; + private static readonly Dictionary _proxyClassDefinitions = new Dictionary(); + + private readonly TypeBuilder _dynamicType; + private readonly HashSet _implementedMethods = new HashSet(); + private readonly List _storageFields = new List(); +#if DEEP_DEBUG || CORECLR + private AssemblyBuilder _dynamicAssembly; +#else + private static AssemblyBuilder _dynamicAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("DynamicClasses"), AssemblyBuilderAccess.Run); +#endif + +#if DEEP_DEBUG + private string _directory; + private string _filename; + private string _fullpath; +#endif + private string _proxyName; + private Type _type; + private MethodInfo OnUnhandledException; + + internal static object Create(Type tInterface, OrderedDictionary> instanceMethods, List delegateMethods, List stubMethods, List usedInstances) { + // now we can calculate the key based on the content of the *Methods collections + var key = tInterface.GetTypeInfo().Assembly.FullName + "::" + tInterface.Name + ":::" + instanceMethods.Keys.Select(each => each.GetTypeInfo().Assembly.FullName + "." + each.FullName + "." + instanceMethods[each].Select(mi => mi.Value.ToSignatureString()).JoinWithComma()).JoinWith(";\r\n") + + "::" + delegateMethods.Select(each => each.GetType().FullName).JoinWith(";\r\n") + + "::" + stubMethods.Select(mi => mi.ToSignatureString()).JoinWithComma(); + // + "!->" + (onUnhandledExceptionMethod == null ? (onUnhandledExceptionDelegate == null ? "GenerateOnUnhandledException" : onUnhandledExceptionDelegate.ToString()) : onUnhandledExceptionMethod.ToSignatureString()); + + return _proxyClassDefinitions.GetOrAdd(key, () => new DynamicType(tInterface, instanceMethods, delegateMethods, stubMethods)).CreateInstance(usedInstances, delegateMethods); + } + + private DynamicType(Type interfaceType, OrderedDictionary> methods, List delegates, List stubs) { + var counter = 0; + + _dynamicType = DefineDynamicType(interfaceType); + + // if the declaring type contains an OUE method declaration, then the host is permitting the plugin to see their oewn unhandled exceptions for debugging purposes. + OnUnhandledException = interfaceType.GetVirtualMethods().FirstOrDefault(each => each.Name == "OnUnhandledException" && each.GetParameterTypes().SequenceEqual(new Type[] {typeof (string), typeof (Exception)})); + + foreach (var instanceType in methods.Keys) { + // generate storage for object + var field = _dynamicType.DefineField("_instance_{0}".format(++counter), instanceType, FieldAttributes.Private); + _storageFields.Add(field); + + // create methods + + foreach (var method in methods[instanceType]) { + _dynamicType.GenerateMethodForDirectCall(method.Key, field, method.Value, OnUnhandledException); + _implementedMethods.Add(method.Key.Name); + } + } + + foreach (var d in delegates) { + var field = _dynamicType.DefineField("_delegate_{0}".format(++counter), d.Key.GetType(), FieldAttributes.Private); + _storageFields.Add(field); + _implementedMethods.Add(d.Value.Name); + + _dynamicType.GenerateMethodForDelegateCall(d.Value, field, OnUnhandledException); + } + + foreach (var method in stubs) { + // did not find a matching method or signature, or the instance told us that it doesn't actually support it + // that's ok, if we get here, it must not be a required method. + // we'll implement a placeholder method for it. + _dynamicType.GenerateStubMethod(method); + } + + _dynamicType.GenerateIsMethodImplemented(); + + // generate the constructor for the class. + DefineConstructor(interfaceType.GetTypeInfo().IsInterface ? typeof (Object) : interfaceType); + } + + internal Type Type { + get { + lock (_proxyClassDefinitions) { + try { + if (_type == null) { +#if !CORECLR + _type = _dynamicType.CreateType(); +#else + _type = _dynamicType.CreateTypeInfo().AsType(); +#endif +#if DEEP_DEBUG + _dynamicAssembly.Save(_filename); +#endif + } + return _type; + } catch (Exception e) { + e.Dump(); + throw; + } + } + } + } + + internal object CreateInstance(List instances, List delegates) { + var proxyConstructor = Type.GetConstructors()[0]; + var instance = proxyConstructor.Invoke(instances.Select(each => each.Value).Concat(delegates.Select(each => each.Key)).ToArray()); + // set the implemented methods collection + var imf = Type.GetField("__implementedMethods", BindingFlags.NonPublic | BindingFlags.Instance); + if (imf != null) { + imf.SetValue(instance, _implementedMethods); + } + + return instance; + } + + private TypeBuilder DefineDynamicType(Type interfaceType) { + lock (_proxyClassDefinitions) { + _proxyName = "{0}_proxy_{1}".format(interfaceType.NiceName().MakeSafeFileName(), _typeCounter++); + } +#if CORECLR + _dynamicAssembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(_proxyName), AssemblyBuilderAccess.Run); +#endif + +#if DEEP_DEBUG + _fullpath = (_proxyName + ".dll").GenerateTemporaryFilename(); + _directory = Path.GetDirectoryName(_fullpath); + _filename = Path.GetFileName(_fullpath); + _dynamicAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(_proxyName), AssemblyBuilderAccess.RunAndSave, _directory); + var dynamicModule = _dynamicAssembly.DefineDynamicModule(_proxyName, _filename); +#else + var dynamicModule = _dynamicAssembly.DefineDynamicModule(_proxyName); +#endif + + // Define a runtime class with specified name and attributes. + if (interfaceType.GetTypeInfo().IsInterface) { + var dynamicType = dynamicModule.DefineType(_proxyName, TypeAttributes.Public, typeof (Object)); + dynamicType.AddInterfaceImplementation(interfaceType); + return dynamicType; + } else { + var dynamicType = dynamicModule.DefineType(_proxyName, TypeAttributes.Public, interfaceType); + return dynamicType; + } + } + + internal void DefineConstructor(Type parentClassType) { + // add constructor that takes the specific type of object that we're going to bind + + var types = _storageFields.Select(each => each.FieldType).ToArray(); + + var constructor = _dynamicType.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, types); + + var il = constructor.GetILGenerator(); + + if (parentClassType != null) { +#if !CORECLR + var basector = parentClassType.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, new Type[0], null); +#else + var constructors = parentClassType.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + var basector = constructors.FirstOrDefault(cons => cons.GetParameters() == null || cons.GetParameters().Length == 0); +#endif + if (basector != null) { + il.LoadArgument(0); + il.Call(basector); + } + } + + var index = 1; + foreach (var backingField in _storageFields) { + // store actualInstance in backingField + il.LoadArgument(0); + il.LoadArgument(index++); + il.StoreField(backingField); + } + // return + il.Return(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Plugin/DynamicTypeExtensions.cs b/src/Microsoft.PackageManagement/Utility/Plugin/DynamicTypeExtensions.cs new file mode 100644 index 000000000..3702d22a6 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Plugin/DynamicTypeExtensions.cs @@ -0,0 +1,315 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Plugin +{ + using System; + using System.Linq; + using System.Collections.Generic; + using System.Reflection; + using System.Reflection.Emit; + using Extensions; + + internal static class DynamicTypeExtensions + { + private static readonly Type[] _emptyTypes = { + }; + + private static MethodInfo _asMethod; + + private static MethodInfo AsMethod + { + get + { + var methods = typeof(DynamicInterfaceExtensions).GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + if (_asMethod == null) + { + _asMethod = methods.FirstOrDefault(method => String.Equals(method.Name, "As", StringComparison.OrdinalIgnoreCase) + && method.GetParameterTypes().Count() == 1 && method.GetParameterTypes().First() == typeof(Object)); + } + + return _asMethod; + } + } + + internal static void OverrideInitializeLifetimeService(this TypeBuilder dynamicType) + { + // add override of InitLifetimeService so this object doesn't fall prey to timeouts + var il = dynamicType.DefineMethod("InitializeLifetimeService", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual, CallingConventions.HasThis, typeof(object), _emptyTypes).GetILGenerator(); + + il.LoadNull(); + il.Return(); + } + + internal static void GenerateIsMethodImplemented(this TypeBuilder dynamicType) + { + // special case -- the IsMethodImplemented method can give the interface owner information as to + // which methods are actually implemented. + var implementedMethodsField = dynamicType.DefineField("__implementedMethods", typeof(HashSet), FieldAttributes.Private); + + var il = dynamicType.CreateMethod("IsMethodImplemented", typeof(bool), typeof(string)); + + il.LoadThis(); + il.LoadField(implementedMethodsField); + il.LoadArgument(1); + il.CallVirutal(typeof(HashSet).GetMethod("Contains")); + il.Return(); + } + + internal static void GenerateMethodForDirectCall(this TypeBuilder dynamicType, MethodInfo method, FieldBuilder backingField, MethodInfo instanceMethod, MethodInfo onUnhandledException) + { + var il = dynamicType.CreateMethod(method); + // the target object has a method that matches. + // let's use that. + + var hasReturn = method.ReturnType != typeof(void); + var hasOue = onUnhandledException != null; + + var exit = il.DefineLabel(); + var setDefaultReturn = il.DefineLabel(); + + var ret = hasReturn ? il.DeclareLocal(method.ReturnType) : null; + var exc = hasOue ? il.DeclareLocal(typeof(Exception)) : null; + + il.BeginExceptionBlock(); + + il.LoadThis(); + il.LoadField(backingField); + + var imTypes = instanceMethod.GetParameterTypes(); + var dmTypes = method.GetParameterTypes(); + + for (var i = 0; i < dmTypes.Length; i++) + { + il.LoadArgument(i + 1); + + // if the types are assignable, + if (imTypes[i].IsAssignableFrom(dmTypes[i])) + { + // it assigns straight across. + } + else + { + // it doesn't, we'll ducktype it. + if (dmTypes[i].GetTypeInfo().IsPrimitive) + { + // box it first? + il.Emit(OpCodes.Box, dmTypes[i]); + } + il.Call(AsMethod.MakeGenericMethod(imTypes[i])); + } + } + + // call the actual method implementation + il.CallVirutal(instanceMethod); + + if (hasReturn) + { + // copy the return value in the return + // check to see if we need to ducktype the return value here. + if (method.ReturnType.IsAssignableFrom(instanceMethod.ReturnType)) + { + // it can store it directly. + } + else + { + // it doesn't assign directly, let's ducktype it. + if (instanceMethod.ReturnType.GetTypeInfo().IsPrimitive) + { + il.Emit(OpCodes.Box, instanceMethod.ReturnType); + } + il.Call(AsMethod.MakeGenericMethod(method.ReturnType)); + } + il.StoreLocation(ret); + } + else + { + // this method isn't returning anything. + if (instanceMethod.ReturnType != typeof(void)) + { + // pop the return value beacuse the generated method is void and the + // method we called actually gave us a result. + il.Emit(OpCodes.Pop); + } + } + il.Emit(OpCodes.Leave_S, exit); + + il.BeginCatchBlock(typeof(Exception)); + if (hasOue) + { + // we're going to call the handler. + il.StoreLocation(exc.LocalIndex); + il.LoadArgument(0); + il.Emit(OpCodes.Ldstr, instanceMethod.ToSignatureString()); + il.LoadLocation(exc.LocalIndex); + il.Call(onUnhandledException); + il.Emit(OpCodes.Leave_S, setDefaultReturn); + } + else + { + // suppress the exception quietly + il.Emit(OpCodes.Pop); + il.Emit(OpCodes.Leave_S, setDefaultReturn); + } + il.EndExceptionBlock(); + + // if we can't return the appropriate value, we're returning default(T) + il.MarkLabel(setDefaultReturn); + SetDefaultReturnValue(il, method.ReturnType); + il.Return(); + + // looks like we're returning the value that we got back from the implementation. + il.MarkLabel(exit); + if (hasReturn) + { + il.LoadLocation(ret.LocalIndex); + } + + il.Return(); + } + + internal static ILGenerator CreateMethod(this TypeBuilder dynamicType, MethodInfo method) + { + return dynamicType.CreateMethod(method.Name, method.ReturnType, method.GetParameterTypes()); + } + + internal static ILGenerator CreateMethod(this TypeBuilder dynamicType, string methodName, Type returnType, params Type[] parameterTypes) + { + var methodBuilder = dynamicType.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, CallingConventions.HasThis, returnType, parameterTypes); + + return methodBuilder.GetILGenerator(); + } + + internal static void GenerateMethodForDelegateCall(this TypeBuilder dynamicType, MethodInfo method, FieldBuilder field, MethodInfo onUnhandledException) + { + var il = dynamicType.CreateMethod(method); + + // the target object has a property or field that matches the signature we're looking for. + // let's use that. + + var delegateType = WrappedDelegate.GetFuncOrActionType(method.GetParameterTypes(), method.ReturnType); + + il.LoadThis(); + il.LoadField(field); + for (var i = 0; i < method.GetParameterTypes().Length; i++) + { + il.LoadArgument(i + 1); + } + il.CallVirutal(delegateType.GetMethod("Invoke")); + il.Return(); + } + + internal static void GenerateStubMethod(this TypeBuilder dynamicType, MethodInfo method) + { + var il = dynamicType.CreateMethod(method); + do + { + if (method.ReturnType != typeof(void)) + { + if (method.ReturnType.GetTypeInfo().IsPrimitive) + { + if (method.ReturnType == typeof(double)) + { + il.LoadDouble(0.0); + break; + } + + if (method.ReturnType == typeof(float)) + { + il.LoadFloat(0.0F); + break; + } + + il.LoadInt32(0); + + if (method.ReturnType == typeof(long) || method.ReturnType == typeof(ulong)) + { + il.ConvertToInt64(); + } + + break; + } + + if (method.ReturnType.GetTypeInfo().IsEnum) + { + // should really find out the actual default? + il.LoadInt32(0); + break; + } + + if (method.ReturnType.GetTypeInfo().IsValueType) + { + var result = il.DeclareLocal(method.ReturnType); + il.LoadLocalAddress(result); + il.InitObject(method.ReturnType); + il.LoadLocation(0); + break; + } + + il.LoadNull(); + } + } while (false); + il.Return(); + } + + private static void SetDefaultReturnValue(ILGenerator il, Type returnType) + { + if (returnType != typeof(void)) + { + if (returnType.GetTypeInfo().IsPrimitive) + { + if (returnType == typeof(double)) + { + il.LoadDouble(0.0); + return; + } + + if (returnType == typeof(float)) + { + il.LoadFloat(0.0F); + return; + } + + il.LoadInt32(0); + + if (returnType == typeof(long) || returnType == typeof(ulong)) + { + il.ConvertToInt64(); + } + + return; + } + + if (returnType.GetTypeInfo().IsEnum) + { + // should really find out the actual default? + il.LoadInt32(0); + return; + } + + if (returnType.GetTypeInfo().IsValueType) + { + var result = il.DeclareLocal(returnType); + il.LoadLocalAddress(result); + il.InitObject(returnType); + il.LoadLocation(result.LocalIndex); + return; + } + + // otherwise load null. + il.LoadNull(); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Plugin/FluentIlExtensions.cs b/src/Microsoft.PackageManagement/Utility/Plugin/FluentIlExtensions.cs new file mode 100644 index 000000000..fcfe528f2 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Plugin/FluentIlExtensions.cs @@ -0,0 +1,171 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Plugin { + using System; + using System.Reflection; + using System.Reflection.Emit; + + internal static class FluentIlExtensions { + private static readonly OpCode[] _loadArgumentInstruction = { + OpCodes.Ldarg_0, OpCodes.Ldarg_1, OpCodes.Ldarg_2, OpCodes.Ldarg_3, OpCodes.Ldarg_S + }; + + private static readonly OpCode[] _storeLocationInstruction = { + OpCodes.Stloc_0, OpCodes.Stloc_1, OpCodes.Stloc_2, OpCodes.Stloc_3, OpCodes.Stloc + }; + + private static readonly OpCode[] _loadLocationInstruction = { + OpCodes.Ldloc_0, OpCodes.Ldloc_1, OpCodes.Ldloc_2, OpCodes.Ldloc_3, OpCodes.Ldloc + }; + + public static void EmitLoadArgument(this ILGenerator il, int argument) { + EmitIndexedInstruction(il, _loadArgumentInstruction, argument); + } + + public static void EmitStoreLocation(this ILGenerator il, int argument) { + EmitIndexedInstruction(il, _storeLocationInstruction, argument); + } + + public static void EmitLoadLocation(this ILGenerator il, int argument) { + EmitIndexedInstruction(il, _loadLocationInstruction, argument); + } + + private static void EmitIndexedInstruction(this ILGenerator il, OpCode[] index, int argument) { + if (argument < index.Length - 1) { + il.Emit(index[argument]); + return; + } + il.Emit(index[index.Length - 1], argument); + } + + public static void Return(this ILGenerator il) { + il.Emit(OpCodes.Ret); + } + + public static void LoadLocation(this ILGenerator il, int location) { + il.EmitLoadLocation(location); + } + + public static void LoadLocation(this ILGenerator il, LocalBuilder local) { + il.EmitLoadLocation(local.LocalIndex); + } + + public static void StoreLocation(this ILGenerator il, int location) { + il.EmitStoreLocation(location); + } + + public static void StoreLocation(this ILGenerator il, LocalBuilder local) { + il.EmitStoreLocation(local.LocalIndex); + } + + public static void LoadArgument(this ILGenerator il, int location) { + il.EmitLoadArgument(location); + } + + public static void LoadThis(this ILGenerator il) { + LoadArgument(il, 0); + } + + public static void Call(this ILGenerator il, ConstructorInfo constructorInfo) { + il.Emit(OpCodes.Call, constructorInfo); + } + + public static void Call(this ILGenerator il, MethodInfo methodInfo) { + il.EmitCall(OpCodes.Call, methodInfo, null); + } + + public static void StoreField(this ILGenerator il, FieldInfo field) { + il.Emit(OpCodes.Stfld, field); + } + + public static void LoadField(this ILGenerator il, FieldInfo field) { + il.Emit(OpCodes.Ldfld, field); + } + + public static void LoadNull(this ILGenerator il) { + il.Emit(OpCodes.Ldnull); + } + + public static void BranchFalse(this ILGenerator il, Label label) { + il.Emit(OpCodes.Brfalse_S, label); + } + + public static void CallVirutal(this ILGenerator il, MethodInfo methodInfo) { + il.Emit(OpCodes.Callvirt, methodInfo); + } + + public static void LoadLocalAddress(this ILGenerator il, LocalBuilder local) { + il.Emit(OpCodes.Ldloca_S, local); + } + + public static void LoadInt32(this ILGenerator il, int value) { + switch (value) { + case 0: + il.Emit(OpCodes.Ldc_I4_0); + break; + case 1: + il.Emit(OpCodes.Ldc_I4_1); + break; + case 2: + il.Emit(OpCodes.Ldc_I4_2); + break; + case 3: + il.Emit(OpCodes.Ldc_I4_3); + break; + case 4: + il.Emit(OpCodes.Ldc_I4_4); + break; + case 5: + il.Emit(OpCodes.Ldc_I4_5); + break; + case 6: + il.Emit(OpCodes.Ldc_I4_6); + break; + case 7: + il.Emit(OpCodes.Ldc_I4_7); + break; + case 8: + il.Emit(OpCodes.Ldc_I4_8); + break; + case -1: + il.Emit(OpCodes.Ldc_I4_M1); + break; + default: + il.Emit(OpCodes.Ldc_I4, value); + break; + } + } + + public static void InitObject(this ILGenerator il, Type t) { + il.Emit(OpCodes.Initobj, t); + } + + public static void LoadFloat(this ILGenerator il, float f) { + il.Emit(OpCodes.Ldc_R4, f); + } + + public static void LoadDouble(this ILGenerator il, double d) { + il.Emit(OpCodes.Ldc_R8, d); + } + + public static void ConvertToInt64(this ILGenerator il) { + il.Emit(OpCodes.Conv_I8); + } + + public static void Box(this ILGenerator il, Type type) { + il.Emit(OpCodes.Box, type); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Plugin/RequiredAttribute.cs b/src/Microsoft.PackageManagement/Utility/Plugin/RequiredAttribute.cs new file mode 100644 index 000000000..60046e868 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Plugin/RequiredAttribute.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Plugin { + using System; + + internal class RequiredAttribute : Attribute { + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Plugin/WrappedDelegate.cs b/src/Microsoft.PackageManagement/Utility/Plugin/WrappedDelegate.cs new file mode 100644 index 000000000..ebfc347b0 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Plugin/WrappedDelegate.cs @@ -0,0 +1,78 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Plugin { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + using Extensions; + + public static class WrappedDelegate { + internal static T CreateProxiedDelegate(this Delegate delegateInstance) { + return (T)(object)typeof(T).CreateWrappedProxy(delegateInstance); + } + + internal static T CreateProxiedDelegate(this object instance, MethodInfo method) { + return (T)(object)CreateProxiedDelegate(instance, method, typeof (T)); + } + + internal static Delegate CreateProxiedDelegate(this object instance, MethodInfo method, Type expectedDelegateType) { + #region DEAD CODE + + // we need our public delegate to be calling an object that is MarshalByRef + // instead, we're creating a delegate thats getting bound to the DTI, which isn't + // extra hoops not needed: + // var actualDelegate = Delegate.CreateDelegate(expectedDelegateType, duckTypedInstance, method); + // var huh = (object)Delegate.CreateDelegate( + // proxyDelegateType, + // actualDelegate.Target, + // actualDelegate.Method, + // true); + + #endregion + + // the func/action type for the proxied delegate. + var proxyDelegateType = GetFuncOrActionType(expectedDelegateType.GetDelegateParameterTypes(), expectedDelegateType.GetDelegateReturnType()); + +#if CORECLR + return method.CreateDelegate(proxyDelegateType, instance); +#else + return Delegate.CreateDelegate(proxyDelegateType, instance, method); +#endif + } + + internal static object CreateWrappedProxy(this Type expectedDelegateType, Delegate dlg) { + // the func/action type for the proxied delegate. + var proxyDelegateType = GetFuncOrActionType(expectedDelegateType.GetDelegateParameterTypes(), expectedDelegateType.GetDelegateReturnType()); + +#if CORECLR + MethodInfo method = dlg.GetMethodInfo(); + return (object)method.CreateDelegate(proxyDelegateType, dlg.Target); +#else + return (object)Delegate.CreateDelegate( + proxyDelegateType, + dlg.Target, + dlg.Method, + true); +#endif + + } + + public static Type GetFuncOrActionType(IEnumerable argTypes, Type returnType) { + return returnType == typeof (void) ? Expression.GetActionType(argTypes.ToArray()) : Expression.GetFuncType(argTypes.ConcatSingleItem(returnType).ToArray()); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Versions/FourPartVersion.cs b/src/Microsoft.PackageManagement/Utility/Versions/FourPartVersion.cs new file mode 100644 index 000000000..a64bc64d4 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Versions/FourPartVersion.cs @@ -0,0 +1,196 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Versions { + using System; + using System.Diagnostics; + using System.Globalization; + using Extensions; + + public struct FourPartVersion : IComparable, IComparable, IEquatable { + private ulong _version; + + public UInt16 Major { + get { + return (UInt16)((_version >> 48) & 0xFFFF); + } + } + + public UInt16 Minor { + get { + return (UInt16)((_version >> 32) & 0xFFFF); + } + } + + public UInt16 Build { + get { + return (UInt16)((_version >> 16) & 0xFFFF); + } + } + + public UInt16 Revision { + get { + return (UInt16)(_version & 0xFFFF); + } + } + + public int CompareTo(object obj) { + return obj is FourPartVersion + ? _version.CompareTo(((FourPartVersion)obj)._version) + : obj is TwoPartVersion + ? _version.CompareTo(((TwoPartVersion)obj)) + : obj is ulong + ? _version.CompareTo((ulong)obj) + : obj is uint + ? _version.CompareTo((uint)obj) + : obj is string + ? _version.CompareTo(((FourPartVersion)(string)obj)._version) + : 0; + } + + public int CompareTo(FourPartVersion other) { + return _version.CompareTo(other._version); + } + + public bool Equals(FourPartVersion other) { + return other._version == _version; + } + + public override string ToString() { + return ULongToString(_version); + } + + public static implicit operator ulong(FourPartVersion version) { + return version._version; + } + + public ulong ToULong() { + return _version; + } + + public static implicit operator Version(FourPartVersion version) { + return new Version((int)((version >> 48) & 0xFFFF), (int)((version >> 32) & 0xFFFF), (int)((version >> 16) & 0xFFFF), (int)((version) & 0xFFFF)); + } + + public static implicit operator string(FourPartVersion version) { + return version.ToString(); + } + + public static implicit operator FourPartVersion(Version version) { + return new FourPartVersion { + _version = StringToULong(version.ToString()) + // _version = ((ulong)version.Major << 48) + ((ulong)version.Minor << 32) + ((ulong)version.Build << 16) + (ulong)version.Revision + }; + } + + public static implicit operator FourPartVersion(ulong version) { + return new FourPartVersion { + _version = version + }; + } + + public static implicit operator FourPartVersion(string version) { + return new FourPartVersion { + _version = StringToULong(version) + }; + } + + public static implicit operator FourPartVersion(DateTime dateAsVersion) { + return new FourPartVersion { + _version = (((UInt64)dateAsVersion.Year) << 48) + (((UInt64)dateAsVersion.Month) << 32) + (((UInt64)dateAsVersion.Day) << 16) + (UInt64)dateAsVersion.TimeOfDay.TotalSeconds + }; + } + + private static string ULongToString(ulong version) { + return String.Format(CultureInfo.CurrentCulture, "{0}.{1}.{2}.{3}", (version >> 48) & 0xFFFF, (version >> 32) & 0xFFFF, (version >> 16) & 0xFFFF, (version) & 0xFFFF); + } + + private static ulong StringToULong(string version) { + if (string.IsNullOrWhiteSpace(version)) { + return 0L; + } + + var vers = version.Split('.'); + var major = vers.Length > 0 ? vers[0].ToInt32(0) : 0; + var minor = vers.Length > 1 ? vers[1].ToInt32(0) : 0; + var build = vers.Length > 2 ? vers[2].ToInt32(0) : 0; + var revision = vers.Length > 3 ? vers[3].ToInt32(0) : 0; + + if (version.Length <= 3 && version.IndexOf('-') > -1) { + return (((UInt64)major) << 48) + (((UInt64)minor) << 32) + (((UInt64)build) << 16) - 1; + } + + return (((UInt64)major) << 48) + (((UInt64)minor) << 32) + (((UInt64)build) << 16) + (UInt64)revision; + } + + public static implicit operator TwoPartVersion(FourPartVersion version) { + return ((uint)(version >> 32)); + } + + public static bool operator ==(FourPartVersion a, FourPartVersion b) { + return a._version == b._version; + } + + public static bool operator !=(FourPartVersion a, FourPartVersion b) { + return a._version != b._version; + } + + public static bool operator <(FourPartVersion a, FourPartVersion b) { + return a._version < b._version; + } + + public static bool operator >(FourPartVersion a, FourPartVersion b) { + return a._version > b._version; + } + + public static bool operator <=(FourPartVersion a, FourPartVersion b) { + return a._version <= b._version; + } + + public static bool operator >=(FourPartVersion a, FourPartVersion b) { + return a._version >= b._version; + } + + public override bool Equals(object o) { + return o is FourPartVersion && Equals((FourPartVersion)o); + } + + public override int GetHashCode() { + return _version.GetHashCode(); + } + + public static implicit operator FourPartVersion(FileVersionInfo versionInfo) { + return new FourPartVersion { + _version = (ulong)((ulong)(uint)versionInfo.FileMajorPart << 48) | (ulong)((ulong)(uint)versionInfo.FileMinorPart << 32) | (ulong)((ulong)(uint)versionInfo.FileBuildPart << 16) | (ulong)((ulong)(uint)versionInfo.FilePrivatePart) + }; + } + + public static FourPartVersion FromFileVersionInfo(FileVersionInfo versionInfo) { + return new FourPartVersion { + _version = (ulong)((ulong)(uint)versionInfo.FileMajorPart << 48) | (ulong)((ulong)(uint)versionInfo.FileMinorPart << 32) | (ulong)((ulong)(uint)versionInfo.FileBuildPart << 16) | (ulong)((ulong)(uint)versionInfo.FilePrivatePart) + }; + } + + public static FourPartVersion Parse(string input) { + return new FourPartVersion { + _version = StringToULong(input) + }; + } + + public static bool TryParse(string input, out FourPartVersion ret) { + ret._version = StringToULong(input); + return true; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/Utility/Versions/TwoPartVersion.cs b/src/Microsoft.PackageManagement/Utility/Versions/TwoPartVersion.cs new file mode 100644 index 000000000..10bdbf150 --- /dev/null +++ b/src/Microsoft.PackageManagement/Utility/Versions/TwoPartVersion.cs @@ -0,0 +1,180 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Utility.Versions { + using System; + using System.Diagnostics; + using System.Globalization; + using Extensions; + + public struct TwoPartVersion : IComparable, IComparable, IEquatable { + private uint _version; + + public UInt16 Major { + get { + return (UInt16)((_version >> 16) & 0xFFFF); + } + } + + public UInt16 Minor { + get { + return (UInt16)(_version & 0xFFFF); + } + } + + public int CompareTo(object obj) { +#if CORECLR + uint compare = 0; + if (obj is TwoPartVersion) + { + compare = ((TwoPartVersion)obj)._version; + } + else if (obj is uint) + { + compare = (uint)obj; + } + else if (obj is string) + { + compare = ((TwoPartVersion)(string)obj)._version; + } + + if (_version < compare) + { + return -1; + } + else if (_version > compare) + { + return 1; + } + else + { + return 0; + } +#else + return obj is TwoPartVersion + ? _version.CompareTo(((TwoPartVersion)obj)._version) + : obj is FourPartVersion + ? _version.CompareTo(((ulong)(FourPartVersion)obj)) + : obj is ulong + ? _version.CompareTo((ulong)obj) + : obj is uint + ? _version.CompareTo((uint)obj) + : obj is string + ? _version.CompareTo(((TwoPartVersion)(string)obj)._version) + : 0; +#endif + } + + public int CompareTo(TwoPartVersion other) { + return _version.CompareTo(other._version); + } + + public bool Equals(TwoPartVersion other) { + return other._version == _version; + } + + public override string ToString() { + return UIntToString(_version); + } + + public static implicit operator uint(TwoPartVersion version) { + return version._version; + } + + public static implicit operator string(TwoPartVersion version) { + return version.ToString(); + } + + public static implicit operator TwoPartVersion(uint version) { + return new TwoPartVersion { + _version = version + }; + } + + public static implicit operator TwoPartVersion(string version) { + return new TwoPartVersion { + _version = StringToUInt(version) + }; + } + + private static string UIntToString(uint version) { + return String.Format(CultureInfo.CurrentCulture, "{0}.{1}", (version >> 16) & 0xFFFF, (version) & 0xFFFF); + } + + private static uint StringToUInt(string version) { + if (string.IsNullOrWhiteSpace(version)) { + return 0; + } + + var vers = version.Split('.'); + var major = vers.Length > 0 ? vers[0].ToInt32(0) : 0; + var minor = vers.Length > 1 ? vers[1].ToInt32(0) : 0; + + return (((uint)major) << 16) + (uint)minor; + } + + public static implicit operator FourPartVersion(TwoPartVersion version) { + return ((ulong)version) << 32; + } + + public static bool operator ==(TwoPartVersion a, TwoPartVersion b) { + return a._version == b._version; + } + + public static bool operator !=(TwoPartVersion a, TwoPartVersion b) { + return a._version != b._version; + } + + public static bool operator <(TwoPartVersion a, TwoPartVersion b) { + return a._version < b._version; + } + + public static bool operator >(TwoPartVersion a, TwoPartVersion b) { + return a._version > b._version; + } + + public static bool operator <=(TwoPartVersion a, TwoPartVersion b) { + return a._version <= b._version; + } + + public static bool operator >=(TwoPartVersion a, TwoPartVersion b) { + return a._version >= b._version; + } + + public override bool Equals(object o) { + return o is TwoPartVersion && Equals((TwoPartVersion)o); + } + + public override int GetHashCode() { + return (int)_version; + } + + public static implicit operator TwoPartVersion(FileVersionInfo versionInfo) { + return new TwoPartVersion { + _version = ((uint)versionInfo.FileMajorPart << 16) | (uint)versionInfo.FileMinorPart + }; + } + + public static TwoPartVersion Parse(string input) { + return new TwoPartVersion { + _version = StringToUInt(input) + }; + } + + public static bool TryParse(string input, out TwoPartVersion ret) { + ret._version = StringToUInt(input); + return true; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/providers/IArchiver.cs b/src/Microsoft.PackageManagement/providers/IArchiver.cs new file mode 100644 index 000000000..dda304ebf --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/IArchiver.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Providers { + using System.Collections.Generic; + using PackageManagement.Internal.Api; + using PackageManagement.Internal.Utility.Plugin; + + public interface IArchiver : IProvider { + /// + /// Returns the name of the Provider. + /// + /// + [Required] + string GetArchiverName(); + + [Required] + IEnumerable UnpackArchive(string localFilename, string destinationFolder, IHostApi requestObject); + + bool IsSupportedFile(string localFilename); + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/providers/IDownloader.cs b/src/Microsoft.PackageManagement/providers/IDownloader.cs new file mode 100644 index 000000000..6029b243c --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/IDownloader.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Providers { + using System; + using PackageManagement.Internal.Api; + using PackageManagement.Internal.Utility.Plugin; + + public interface IDownloader : IProvider { + /// + /// Returns the name of the Provider. + /// + /// + [Required] + string GetDownloaderName(); + + [Required] + string DownloadFile(Uri remoteLocation, string localFilename, int timeoutMilliseconds, bool showProgress, IHostApi requestObject); + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/providers/IMetaProvider.cs b/src/Microsoft.PackageManagement/providers/IMetaProvider.cs new file mode 100644 index 000000000..b4fd3de7d --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/IMetaProvider.cs @@ -0,0 +1,83 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Providers { + using System; + using System.Collections.Generic; + using Api; + using PackageManagement.Internal.Utility.Plugin; + + public interface IMetaProvider : IProvider { + #region declare MetaProvider-interface + + /* Synced/Generated code =================================================== */ + + /// + /// Will instantiate an instance of a provider given it's name. + /// + /// If the name is a filename, this will ask the provider to attempt to load it. + /// + /// the name of the provider to create + /// an instance of the provider. + [Required] + object CreateProvider(string name); + + /// + /// Gets the name of this MetaProvider + /// + /// the name of the MetaProvider. + [Required] + string GetMetaProviderName(); + + /// + /// Returns a collection of all the names of Providers this MetaProvider can create. + /// + /// a collection of all the names of Providers this MetaProvider can create + [Required] + IEnumerable GetProviderNames(); + + /// + /// Returns the path to the file that defines a given provider. + /// + /// the path of the provider + [Required] + string GetProviderPath(string name); + + /// + /// Return available providers that not loaded yet. + /// + /// Object inherits IRequest + /// Name of the provider + /// Retrieves only the specified version of the provider + /// Retrieves only a version of the provider that is greater than or equal to the specified value + /// Retrieves only a version of the provider that is less than the specified value + /// + /// + [Required] + void RefreshProviders(IRequest request, string providerName, Version requiredVersion, Version minimumVersion, Version maximumVersion, bool logWarning); + + /// + /// Load a particular provider written in powershell module. + /// + /// Object inherits IRequest + /// The file path of the PowerShell module provider + /// Retrieves only the specified version of the provider + /// Force to load a provider + /// + [Required] + IEnumerable LoadAvailableProvider(IRequest request, string modulePath, Version requiredVersion, bool force); + + #endregion + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/providers/IPackageProvider.cs b/src/Microsoft.PackageManagement/providers/IPackageProvider.cs new file mode 100644 index 000000000..44549e063 --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/IPackageProvider.cs @@ -0,0 +1,328 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Providers { + using System; + using PackageManagement.Internal.Api; + using PackageManagement.Internal.Utility.Plugin; + + public interface IPackageProvider : IProvider { + /// + /// Returns the name of the Provider. + /// + /// + /// May be implemented as a property: string PackageProviderName { get; } + /// + /// the name of the package provider + /// + /// PowerShellMetaProvider notes: + /// - may use Get-PackageProviderName as the function name + /// + [Required] + string GetPackageProviderName(); + + /// + /// This is called when the user is adding (or updating) a package source + /// If this PROVIDER doesn't support user-defined package sources, remove this method. + /// + /// + /// The name of the package source. If this parameter is null or empty the PROVIDER should use the + /// location as the name (if the PROVIDER actually stores names of package sources) + /// + /// + /// The location (ie, directory, URL, etc) of the package source. If this is null or empty, the + /// PROVIDER should use the name as the location (if valid) + /// + /// + /// A boolean indicating that the user trusts this package source. Packages returned from this source + /// should be marked as 'trusted' + /// + /// + /// An object passed in from the CORE that contains functions that can be used to interact with + /// the CORE and HOST + /// + /// + /// Make 'trusted' a dynamic parameter. + /// PowerShellMetaProvider notes: + /// - may use 'Add-PackageSource' as the function name. + /// - $request object is not passed as a parameter, but inserted as a variable before the call. + /// - Values can be returned using Write-Object for objects returned from the New-PackageSource function + /// +#if NEW_SIGNATURES + void AddPackageSource(string name, string location, IRequest requestObject); +#endif + +#if !REMOVE_DEPRECATED_FUNCTIONS + void AddPackageSource(string name, string location, bool trusted, IRequest requestObject); +#endif + /// + /// Resolves and returns package sources. + /// + /// Expected behavior: + /// If request.Sources is null or empty, the provider should return the list of registered sources. + /// Otherwise, the provider should return package sources that match the strings in the request.Sources + /// collction. If the string doesn't match a registered source (either by name or by location) and the + /// string can be construed as a package source (ie, passing in a valid URL to a provider that uses URLs + /// for their package sources), then the provider should return a package source for that URL, but marked + /// as 'unregistered' (and 'untrusted') + /// + /// + /// Data is returned using the request.YieldPackageSource(...) function. + /// + /// The request context passed to the provider. + /// + /// PowerShellMetaProvider notes: + /// - may use Resolve-PackageSource as the function name + /// - $request object is not passed as a parameter, but inserted as a variable before the ca New-PackageSourcell. + /// - Values can be returned using Write-Object for objects returned from the function + /// + void ResolvePackageSources(IRequest requestObject); + + /// + /// Removes a registered package source. + /// + /// Matching of the package source should be done on via the name or location against the specified string. + /// + /// the name or location of the source to be removed. + /// The request context passed to the provider. + /// + /// PowerShellMetaProvider notes: + /// - may use 'Remove-PackageSource' as the function name. + /// - request object is not passed as a parameter, but inserted as a variable before the call. + /// + void RemovePackageSource(string source, IRequest requestObject); + + /// + /// Finds packages given a set of criteria. + /// + /// If request.Sources is null or empty, the provider should use all the registered sources specified. + /// + /// If the provider doesn't support package sources, and the request.Sources isn't null or empty, + /// the provider should return nothing and exit immediately. + /// + /// If request.Sources contains one or more elements, the repository should scope the search to just repositories + /// that match sources (either by name or location) in the list of specified strings. + /// + /// matches against the name of a package. If this is null or empty, should return all matching packages. + /// an exact version of the package to match. If this is specified, miniumum and maximum should be ignored. + /// the minimum version of the package to match. If requiredVersion is specified, this should be ignored. + /// the maximum version of the package to match. If requiredVersion is specified, this should be ignored. + /// the batch id. If the provider supports batch searches, and this is non-zero, + /// the provider should queue up the search to be executed when CompleteFind is called + /// The request context passed to the provider. + /// + /// Returns Software Identities thru the use of request.YieldSoftwareIdentity(), and extended data via: + /// AddMetadata(...) -- adds key/value pairs to an element + /// AddMeta(...) -- adds a new Meta element child to an element + /// AddEntity(...) -- adds a new Entity to an element + /// AddLink(...) -- adds a new Link to a SoftwareIdentity + /// AddDependency(...) -- adds a new 'requires' Link to a SoftwareIdentity + /// AddPayload(...) -- adds a Payload (or returns the existing one) to a SoftwareIdentity + /// AddEvidence(...) -- adds an Evidence (or returns the existing one) to a SoftwareIdentity + /// AddDirectory(...) -- adds a Directory to a Payload, Evidence or Directory + /// AddFile(...) -- adds a File to a Payload, Evidence or Directory + /// AddProcess(...) -- adds a Process to a Payload or Evidence + /// AddResource(...) -- adds a Resource to a Payload or Evidence + /// + /// + /// PowerShellMetaProvider notes: + /// - may use 'Find-Package' as the function name. + /// - If the provider supports batch operations, the name should be a string array + /// - Values can be returned using Write-Object for objects returned from the New-SoftwareIdentity function + /// +#if NEW_SIGNATURES + void FindPackage(string name, string requiredVersion, string minimumVersion, string maximumVersion, IRequest requestObject); + void FindPackage(string[] names, string requiredVersion, string minimumVersion, string maximumVersion, IRequest requestObject); +#endif + +#if !REMOVE_DEPRECATED_FUNCTIONS + void FindPackage(string name, string requiredVersion, string minimumVersion, string maximumVersion,int id, IRequest requestObject); +#endif + + /// + /// Returns SoftwareIdentities given a local filename. + /// + /// the full path to the file to evaluate as a package + /// the batch id. If the provider supports batch searches, and this is non-zero, + /// the provider should queue up the search to be executed when CompleteFind is called + /// The request context passed to the provider. + /// + /// Returns Software Identities thru the use of request.YieldSoftwareIdentity(), and extended data via: + /// AddMetadata(...) -- adds key/value pairs to an element + /// AddMeta(...) -- adds a new Meta element child to an element + /// AddEntity(...) -- adds a new Entity to an element + /// AddLink(...) -- adds a new Link to a SoftwareIdentity + /// AddDependency(...) -- adds a new 'requires' Link to a SoftwareIdentity + /// AddPayload(...) -- adds a Payload (or returns the existing one) to a SoftwareIdentity + /// AddEvidence(...) -- adds an Evidence (or returns the existing one) to a SoftwareIdentity + /// AddDirectory(...) -- adds a Directory to a Payload, Evidence or Directory + /// AddFile(...) -- adds a File to a Payload, Evidence or Directory + /// AddProcess(...) -- adds a Process to a Payload or Evidence + /// AddResource(...) -- adds a Resource to a Payload or Evidence + /// + /// + /// PowerShellMetaProvider notes: + /// - may use 'Find-PackageByFile' as the function name. + /// - If the provider supports batch operations, the file parameter should be a string array + /// - Values can be returned using Write-Object for objects returned from the New-SoftwareIdentity function + /// +#if NEW_SIGNATURES + void FindPackageByFile(string filePath, IRequest requestObject); + void FindPackageByFile(string[] filePaths, IRequest requestObject); +#endif + +#if !REMOVE_DEPRECATED_FUNCTIONS + void FindPackageByFile(string filePath, int id, IRequest requestObject); +#endif + + /// + /// Returns SoftwareIdentities given a URI. + /// + /// the URI to search for packages + /// the batch id. If the provider supports batch searches, and this is non-zero, + /// the provider should queue up the search to be executed when CompleteFind is called + /// The request context passed to the provider. + /// + /// Returns Software Identities thru the use of request.YieldSoftwareIdentity(), and extended data via: + /// AddMetadata(...) -- adds key/value pairs to an element + /// AddMeta(...) -- adds a new Meta element child to an element + /// AddEntity(...) -- adds a new Entity to an element + /// AddLink(...) -- adds a new Link to a SoftwareIdentity + /// AddDependency(...) -- adds a new 'requires' Link to a SoftwareIdentity + /// AddPayload(...) -- adds a Payload (or returns the existing one) to a SoftwareIdentity + /// AddEvidence(...) -- adds an Evidence (or returns the existing one) to a SoftwareIdentity + /// AddDirectory(...) -- adds a Directory to a Payload, Evidence or Directory + /// AddFile(...) -- adds a File to a Payload, Evidence or Directory + /// AddProcess(...) -- adds a Process to a Payload or Evidence + /// AddResource(...) -- adds a Resource to a Payload or Evidence + /// + /// + /// PowerShellMetaProvider notes: + /// - may use 'Find-PackageByUri' as the function name. + /// - If the provider supports batch operations, the uri parameter should be an array + /// - Values can be returned using Write-Object for objects returned from the New-SoftwareIdentity function + /// +#if NEW_SIGNATURES + void FindPackageByUri(Uri uri, IRequest requestObject); + void FindPackageByUri(Uri[] uris, IRequest requestObject); +#endif + +#if !REMOVE_DEPRECATED_FUNCTIONS + void FindPackageByUri(Uri uri, int id, IRequest requestObject); +#endif + + /// + /// Returns Software Identities for installed packages + /// + /// matches against the name of a package. If this is null or empty, should return all matching packages. + /// an exact version of the package to match. If this is specified, miniumum and maximum should be ignored. + /// the minimum version of the package to match. If requiredVersion is specified, this should be ignored. + /// the maximum version of the package to match. If requiredVersion is specified, this should be ignored. + /// The request context passed to the provider. + /// + /// Returns Software Identities thru the use of request.YieldSoftwareIdentity(), and extended data via: + /// AddMetadata(...) -- adds key/value pairs to an element + /// AddMeta(...) -- adds a new Meta element child to an element + /// AddEntity(...) -- adds a new Entity to an element + /// AddLink(...) -- adds a new Link to a SoftwareIdentity + /// AddDependency(...) -- adds a new 'requires' Link to a SoftwareIdentity + /// AddPayload(...) -- adds a Payload (or returns the existing one) to a SoftwareIdentity + /// AddEvidence(...) -- adds an Evidence (or returns the existing one) to a SoftwareIdentity + /// AddDirectory(...) -- adds a Directory to a Payload, Evidence or Directory + /// AddFile(...) -- adds a File to a Payload, Evidence or Directory + /// AddProcess(...) -- adds a Process to a Payload or Evidence + /// AddResource(...) -- adds a Resource to a Payload or Evidence + /// + /// + /// PowerShellMetaProvider notes: + /// - may use 'Get-InstalledPackage' as the function name. + /// - Values can be returned using Write-Object for objects returned from the New-SoftwareIdentity function + /// + void GetInstalledPackages(string name, string requiredVersion, string minimumVersion, string maximumVersion, IRequest requestObject); + + /// + /// If supported, this should download a package file to the location provided. + /// + /// + /// + /// The request context passed to the provider. + /// + /// PowerShellMetaProvider notes: + /// - may use 'Download-Package' as the function name. + /// + void DownloadPackage(string fastPath, string location, IRequest requestObject); + + /// + /// (not currently used) + /// Will be used to allow packages to return full details on demand, rather than at find/get time. + /// + /// the round-tripped (from FindPackageXXX or GetInstalledPackages ) identity of the package + /// The request context passed to the provider. + /// + /// PowerShellMetaProvider notes: + /// - May use 'Get-PackageDetail' as the function name. + /// + void GetPackageDetails(string fastPath, IRequest requestObject); + + /// + /// Installs a package. + /// + /// the round-tripped (from FindPackageXXX) identity of the package to be installed + /// The request context passed to the provider. + /// + /// Returns Software Identities thru the use of request.YieldSoftwareIdentity(), and extended data via: + /// AddMetadata(...) -- adds key/value pairs to an element + /// AddMeta(...) -- adds a new Meta element child to an element + /// AddEntity(...) -- adds a new Entity to an element + /// AddLink(...) -- adds a new Link to a SoftwareIdentity + /// AddDependency(...) -- adds a new 'requires' Link to a SoftwareIdentity + /// AddPayload(...) -- adds a Payload (or returns the existing one) to a SoftwareIdentity + /// AddEvidence(...) -- adds an Evidence (or returns the existing one) to a SoftwareIdentity + /// AddDirectory(...) -- adds a Directory to a Payload, Evidence or Directory + /// AddFile(...) -- adds a File to a Payload, Evidence or Directory + /// AddProcess(...) -- adds a Process to a Payload or Evidence + /// AddResource(...) -- adds a Resource to a Payload or Evidence + /// + /// + /// PowerShellMetaProvider notes: + /// - May use 'Install-Package' as the function name. + /// + void InstallPackage(string fastPath, IRequest requestObject); + + /// + /// Uninstalls a package. + /// + /// the round-tripped (from GetInstalledPackages) identity of the package to be uninstalled + /// The request context passed to the provider. + /// + /// Returns Software Identities thru the use of request.YieldSoftwareIdentity(), and extended data via: + /// AddMetadata(...) -- adds key/value pairs to an element + /// AddMeta(...) -- adds a new Meta element child to an element + /// AddEntity(...) -- adds a new Entity to an element + /// AddLink(...) -- adds a new Link to a SoftwareIdentity + /// AddDependency(...) -- adds a new 'requires' Link to a SoftwareIdentity + /// AddPayload(...) -- adds a Payload (or returns the existing one) to a SoftwareIdentity + /// AddEvidence(...) -- adds an Evidence (or returns the existing one) to a SoftwareIdentity + /// AddDirectory(...) -- adds a Directory to a Payload, Evidence or Directory + /// AddFile(...) -- adds a File to a Payload, Evidence or Directory + /// AddProcess(...) -- adds a Process to a Payload or Evidence + /// AddResource(...) -- adds a Resource to a Payload or Evidence + /// + /// + /// PowerShellMetaProvider notes: + /// - May use 'Uninstall-Package' as the function name. + /// + void UninstallPackage(string fastPath, IRequest requestObject); + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/providers/IProvider.cs b/src/Microsoft.PackageManagement/providers/IProvider.cs new file mode 100644 index 000000000..a6003225e --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/IProvider.cs @@ -0,0 +1,65 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Internal.Providers { + using System; + using PackageManagement.Internal.Api; + using PackageManagement.Internal.Utility.Plugin; + + public interface IProvider { + /// + /// Allows the Provider to do one-time initialization. + /// This is called after the Provider is instantiated . + /// + /// Object implementing some or all IRequest methods + [Required] + void InitializeProvider(IRequest requestObject); + + /// + /// Gets the features advertized from the provider + /// + /// + void GetFeatures(IRequest requestObject); + + /// + /// Gets dynamically defined options from the provider + /// + /// + /// + void GetDynamicOptions(string category, IRequest requestObject); + + /// + /// Allows runtime examination of the implementing class to check if a given method is implemented. + /// + /// + /// + bool IsMethodImplemented(string methodName); + + /// + /// Returns the version of the provider. + /// This is expected to be in multipart numeric format. + /// + /// The version of the provider + string GetProviderVersion(); + + + /// + /// By declaring this, the DuckTyper will write isolation code that ensures that functions can't throw exceptions, instead + /// Exceptions can be directed to the OnUnhandledException function. + /// + /// + /// + void OnUnhandledException(string methodName, Exception exception); + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/CollectionExtensions.cs b/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/CollectionExtensions.cs new file mode 100644 index 000000000..95683c545 --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/CollectionExtensions.cs @@ -0,0 +1,175 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Provider.Utility { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + using System.Threading.Tasks; + using Internal.Utility.Collections; + + internal static class CollectionExtensions { + private static readonly MethodInfo _castMethod = typeof (Enumerable).GetMethod("Cast"); + private static readonly MethodInfo _toArrayMethod = typeof (Enumerable).GetMethod("ToArray"); + private static readonly IDictionary _castMethods = new Dictionary(); + private static readonly IDictionary _toArrayMethods = new Dictionary(); + + /// + /// Determines whether the collection object is either null or an empty collection. + /// + /// + /// The collection. + /// + /// true if [is null or empty] [the specified collection]; otherwise, false . + /// + /// + /// + public static bool IsNullOrEmpty(this IEnumerable collection) { + return collection == null || !collection.Any(); + } + + /// + /// Concatenates a single item to an IEnumerable + /// + /// + /// + /// + /// + public static IEnumerable ConcatSingleItem(this IEnumerable enumerable, T item) { + return enumerable.Concat(new[] { + item + }); + } + + public static IEnumerable WhereNotNull(this IEnumerable enumerable) { + return enumerable == null ? Enumerable.Empty() : enumerable.Where(each => (object)each != null); + } + + public static IEnumerable ToIEnumerable(this IEnumerator enumerator) { + if (enumerator != null) { + while (enumerator.MoveNext()) { + yield return enumerator.Current; + } + } + } + + public static IEnumerable Concat(this IEnumerable set1, IEnumerator set2) { + var s1 = set1 ?? Enumerable.Empty(); + var s2 = set2 == null ? Enumerable.Empty() : set2.ToIEnumerable(); + return s1.Concat(s2); + } + + public static IEnumerable SingleItemAsEnumerable(this T item) { + return new T[] { + item + }; + } + + public static void AddRangeLocked(this List list, IEnumerable items) { + if (list == null) { + throw new ArgumentNullException("list"); + } + lock (list) { + list.AddRange(items); + } + } + + //public static void ParallelForEach(this IEnumerable enumerable, Action action) { + // var items = enumerable.ReEnumerable(); + // var first = items.FirstOrDefault(); + // if (first != null) { + // object second = items.Skip(1).FirstOrDefault(); + // if (second != null) { + // Parallel.ForEach(items, new ParallelOptions { + // MaxDegreeOfParallelism = -1, + // TaskScheduler = new ThreadPerTaskScheduler() + // }, action); + // } else { + // action(first); + // } + // } + //} + + public static void SerialForEach(this IEnumerable enumerable, Action action) { + var items = enumerable.ReEnumerable(); + object first = items.FirstOrDefault(); + if (first != null) { + object second = items.Skip(1).FirstOrDefault(); + if (second != null) { + foreach (var item in items) { + action(item); + } + } else { + action(items.FirstOrDefault()); + } + } + } + + public static IEnumerable AsyncForEach(this IEnumerable enumerable, Action action) { + return enumerable.Select(i => Task.Factory.StartNew(() => action(i))); + } + + public static void WaitAll(this IEnumerable tasks) { + Task.WaitAll(tasks.ToArray()); + } + + //public static object ToIEnumerableT(this IEnumerable enumerable, Type elementType) { + // return _castMethods.GetOrAdd(elementType, () => _castMethod.MakeGenericMethod(elementType)).Invoke(null, new object[] {enumerable}); + //} + + //public static object ToArrayT(this IEnumerable enumerable, Type elementType) { + // return _toArrayMethods.GetOrAdd(elementType, () => _toArrayMethod.MakeGenericMethod(elementType)).Invoke(null, new[] {enumerable.ToIEnumerableT(elementType)}); + //} + + /// + /// returns the index position where the predicate matches the value + /// + /// + /// + /// + /// + public static int IndexWhere(this IEnumerable source, Func predicate) { + if (source != null && predicate != null) { + var index = -1; + if (source.Any(element => predicate(element, ++index))) { + return index; + } + } + return -1; + } + + /// + /// returns the index position where the predicate matches the value + /// + /// + /// + /// + /// + public static int IndexWhere(this IEnumerable source, Func predicate) { + if (source != null && predicate != null) { + var index = -1; + if (source.Any(element => { + ++index; + return predicate(element); + })) { + return index; + } + } + return -1; + } + } + + // Provides a task scheduler that dedicates a thread per task. +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/EnumerableExtensions.cs b/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/EnumerableExtensions.cs new file mode 100644 index 000000000..e052d8192 --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/EnumerableExtensions.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Provider.Utility { + using System; + using System.Collections.Generic; + using System.Linq; + + internal static class EnumerableExtensions { + /// + /// Returns a ReEnumerable wrapper around the collection which timidly (cautiously) pulls items + /// but still allows you to to re-enumerate without re-running the query. + /// + /// + /// + /// + public static MutableEnumerable ReEnumerable(this IEnumerable collection) { + if (collection == null) { + return new ReEnumerable(Enumerable.Empty()); + } + return collection as MutableEnumerable ?? new ReEnumerable(collection); + } + + public static IEnumerable FilterWithFinalizer(this IEnumerable source, Func predicate, Action onFilterAction) { + foreach (var i in source) { + if (predicate(i)) { + onFilterAction(i); + } else { + yield return i; + } + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/EqualityComparer.cs b/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/EqualityComparer.cs new file mode 100644 index 000000000..c9197a144 --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/EqualityComparer.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Provider.Utility { + using System; + using System.Collections.Generic; + + internal class EqualityComparer : IEqualityComparer { + private readonly Func _compareFn; + private readonly Func _hashFn; + + public EqualityComparer(Func compareFn, Func hashFn) { + _compareFn = compareFn; + _hashFn = hashFn; + } + + public EqualityComparer(Func compareFn) { + _compareFn = compareFn; + _hashFn = (obj) =>obj.GetHashCode(); + } + + public bool Equals(T x, T y) { + return _compareFn(x, y); + } + + public int GetHashCode(T obj) { + return _hashFn(obj); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/Extensions.cs b/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/Extensions.cs new file mode 100644 index 000000000..d7bb1a00f --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/Extensions.cs @@ -0,0 +1,266 @@ +namespace Microsoft.PackageManagement.Provider.Utility +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Net.Http; + using System.Text; + using System.Text.RegularExpressions; + using System.Threading.Tasks; + using System.Xml.Linq; + + internal static class HttpContentExtensions { + + internal static async Task ReadAsFileAsync(this HttpContent content, string fileName) { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentException(fileName); + } + + //Get the absolute path + var pathName = Path.GetFullPath(fileName); + + using (var fileStream = new FileStream(pathName, FileMode.Create, FileAccess.Write, FileShare.None)) { + await content.CopyToAsync(fileStream); + return fileStream.Length; + } + } + } + + internal static class XElementExtensions + { + internal static IEnumerable ElementsNoNamespace(this XContainer container, string localName) + { + return container.Elements().Where(e => e.Name.LocalName == localName); + } + + /// + /// Get the optional attribute value depends on the localName and the nameSpace. Returns null if we can't find it + /// + /// + /// + /// + /// + internal static string GetOptionalAttributeValue(this XElement element, string localName, string namespaceName = null) + { + XAttribute attr; + // Name space is null so don't use it + if (String.IsNullOrWhiteSpace(namespaceName)) + { + attr = element.Attribute(localName); + } + else + { + attr = element.Attribute(XName.Get(localName, namespaceName)); + } + + return attr != null ? attr.Value : null; + } + } + + internal static class VersionExtensions + { + internal static IEnumerable GetComparableVersionStrings(this SemanticVersion version) + { + Version coreVersion = version.Version; + string specialVersion = String.IsNullOrWhiteSpace(version.SpecialVersion) ? String.Empty : "-" + version.SpecialVersion; + string[] originalVersionComponents = version.GetOriginalVersionComponents(); + + + if (coreVersion.Revision == 0) + { + if (coreVersion.Build == 0) + { + yield return String.Format( + CultureInfo.InvariantCulture, + "{0}.{1}{2}", + originalVersionComponents[0], + originalVersionComponents[1], + specialVersion); + } + + yield return String.Format( + CultureInfo.InvariantCulture, + "{0}.{1}.{2}{3}", + originalVersionComponents[0], + originalVersionComponents[1], + originalVersionComponents[2], + specialVersion); + } + + yield return String.Format( + CultureInfo.InvariantCulture, + "{0}.{1}.{2}.{3}{4}", + originalVersionComponents[0], + originalVersionComponents[1], + originalVersionComponents[2], + originalVersionComponents[3], + specialVersion); + + } + } + + internal static class ObjectExtensions + { + internal static string ToStringSafe(this object obj) + { + return obj == null ? null : obj.ToString(); + } + } + + internal static class StringExtensions + { + internal static string SafeTrim(this string value) + { + return value == null ? null : value.Trim(); + } + + internal static string ToBase64(this string text) + { + if (text == null) + { + return null; + } + return Convert.ToBase64String(text.ToByteArray()); + } + + internal static string FromBase64(this string text) + { + if (text == null) + { + return null; + } + return Convert.FromBase64String(text).ToUtf8String(); + } + + internal static string FixVersion(this string versionString) + { + if (!String.IsNullOrWhiteSpace(versionString)) + { + if (versionString[0] == '.') + { + // make sure we have a leading zero when someone says .5 + versionString = "0" + versionString; + } + + if (versionString.IndexOf('.') == -1) + { + // make sure we make a 1 work like 1.0 + versionString = versionString + ".0"; + } + } + + return versionString; + } + + // Encodes the string as an array of UTF8 bytes. + private static byte[] ToByteArray(this string text) + { + return Encoding.UTF8.GetBytes(text); + } + + // Creates a string from a collection of UTF8 bytes + private static string ToUtf8String(this IEnumerable bytes) + { + var data = bytes.ToArray(); + try + { + return Encoding.UTF8.GetString(data); + } + finally + { + Array.Clear(data, 0, data.Length); + } + } + + public static bool EqualsIgnoreCase(this string str, string str2) + { + if (str == null && str2 == null) + { + return true; + } + + if (str == null || str2 == null) + { + return false; + } + + return str.Equals(str2, StringComparison.OrdinalIgnoreCase); + } + + public static bool IsTrue(this string text) + { + return !string.IsNullOrWhiteSpace(text) && text.Equals("true", StringComparison.CurrentCultureIgnoreCase); + } + + public static bool CompareVersion(this string version1, string version2) + { + if(string.IsNullOrWhiteSpace(version1) && string.IsNullOrWhiteSpace(version2)) + { + return true; + } + if (string.IsNullOrWhiteSpace(version1) || string.IsNullOrWhiteSpace(version2)) + { + return false; + } + + var semver1 = new SemanticVersion(version1); + var semver2 = new SemanticVersion(version2); + return (semver1 == semver2); + } + } + + internal static class LinqExtensions + { + internal static Dictionary ToDictionaryNicely(this IEnumerable source, Func keySelector, Func elementSelector, IEqualityComparer comparer) + { + if (source == null) + { + throw new ArgumentNullException("source"); + } + if (keySelector == null) + { + throw new ArgumentNullException("keySelector"); + } + if (elementSelector == null) + { + throw new ArgumentNullException("elementSelector"); + } + + var d = new Dictionary(comparer); + foreach (var element in source) + { + d.AddOrSet(keySelector(element), elementSelector(element)); + } + return d; + } + + internal static TValue AddOrSet(this IDictionary dictionary, TKey key, TValue value) + { + lock (dictionary) + { + if (dictionary.ContainsKey(key)) + { + dictionary[key] = value; + } + else + { + dictionary.Add(key, value); + } + } + return value; + } + + internal static TSource SafeAggregate(this IEnumerable source, Func func) + { + var src = source.ToArray(); + if (source != null && src.Any()) + { + return src.Aggregate(func); + } + return default(TSource); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/FilesystemExtensions.cs b/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/FilesystemExtensions.cs new file mode 100644 index 000000000..5cb3b2d91 --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/FilesystemExtensions.cs @@ -0,0 +1,268 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Provider.Utility { + using System; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Text; + using System.Text.RegularExpressions; + + internal static class FilesystemExtensions { + private static readonly char[] _pathCharacters = "/\\".ToCharArray(); + private static int _counter = Process.GetCurrentProcess().Id << 16; + public static string OriginalTempFolder; + + static FilesystemExtensions() { + OriginalTempFolder = OriginalTempFolder ?? Path.GetTempPath(); + // ResetTempFolder(); + } + + public static string TempPath {get; private set;} + + public static int Counter { + get { + return ++_counter; + } + } + + public static string CounterHex { + get { + return Counter.ToString("x8", CultureInfo.CurrentCulture); + } + } + + public static bool LooksLikeAFilename(this string text) { + return text.IndexOfAny(_pathCharacters) > -1; + } + + public static void TryHardToDelete(this string location) + { + if (Directory.Exists(location)) + { + try + { + Directory.Delete(location, true); + } + catch + { + // didn't take, eh? + } + } + + if (File.Exists(location)) + { + try + { + File.Delete(location); + } + catch + { + // didn't take, eh? + } + } + + // if it is still there, move and mark it. + if (File.Exists(location) || Directory.Exists(location)) + { + try + { + // move the file to the tmp file + // and tell the OS to remove it next reboot. + var tmpFilename = location.GenerateTemporaryFilename() + ".delete_me"; // generates a unique filename but not a file! + //MoveFileOverwrite(location, tmpFilename); + + if (File.Exists(location) || Directory.Exists(location)) + { + // of course, if the tmpFile isn't on the same volume as the location, this doesn't work. + // then, last ditch effort, let's rename it in the current directory + // and then we can hide it and mark it for cleanup . + tmpFilename = Path.Combine(Path.GetDirectoryName(location), "tmp." + CounterHex + "." + Path.GetFileName(location) + ".delete_me"); + //MoveFileOverwrite(location, tmpFilename); + if (File.Exists(tmpFilename) || Directory.Exists(location)) + { + // hide the file for convenience. + File.SetAttributes(tmpFilename, File.GetAttributes(tmpFilename) | FileAttributes.Hidden); + } + } + + // Now we mark the locked file to be deleted upon next reboot (or until another coapp app gets there) + // MoveFileOverwrite(File.Exists(tmpFilename) ? tmpFilename : location, null); + } + catch + { + // really. Hmmm. + } + + if (File.Exists(location)) + { + // err("Unable to forcably remove file '{0}'. This can't be good.", location); + } + } + return; + } + + ///// + ///// File move abstraction that can be implemented to handle non-windows platforms + ///// + ///// + ///// + //public static void MoveFileOverwrite(string sourceFile, string destinationFile) { + // NativeMethods.MoveFileEx(sourceFile, destinationFile, MoveFileFlags.ReplaceExisting); + //} + + //public static void MoveFileAtNextBoot(string sourceFile, string destinationFile) { + // NativeMethods.MoveFileEx(sourceFile, destinationFile, MoveFileFlags.DelayUntilReboot); + //} + + public static string GenerateTemporaryFilename(this string filename) { + string ext = null; + string name = null; + string folder = null; + + if (!string.IsNullOrWhiteSpace(filename)) { + ext = Path.GetExtension(filename); + name = Path.GetFileNameWithoutExtension(filename); + folder = Path.GetDirectoryName(filename); + } + + if (string.IsNullOrWhiteSpace(ext)) { + ext = ".tmp"; + } + if (string.IsNullOrWhiteSpace(folder)) { + folder = TempPath; + } + + do + { + name = Path.Combine(folder, "tmpFile." + Path.GetRandomFileName() + (string.IsNullOrWhiteSpace(name) ? ext : "." + name + ext)); + } + while (File.Exists(name)); + + // return MarkFileTemporary(name); + return name; + } + + //public static void ResetTempFolder() { + // // set the temporary folder to be a child of the User temporary folder + // // based on the application name + // var appName = typeof(FilesystemExtensions).GetTypeInfo().Assembly.GetName().Name; + // if (OriginalTempFolder.IndexOf(appName, StringComparison.CurrentCultureIgnoreCase) == -1) { + // var appTempPath = Path.Combine(OriginalTempFolder, appName); + // if (!Directory.Exists(appTempPath)) { + // Directory.CreateDirectory(appTempPath); + // } + + // TempPath = Path.Combine(appTempPath, (Directory.EnumerateDirectories(appTempPath).Count() + 1).ToString(CultureInfo.CurrentCulture)); + // if (!Directory.Exists(TempPath)) { + // Directory.CreateDirectory(TempPath); + // } + + // // make sure this temp directory gets marked for eventual cleanup. + // MoveFileOverwrite(appTempPath, null); + // MoveFileAtNextBoot(TempPath, null); + // } + + // TempPath = TempPath ?? OriginalTempFolder; + //} + + /// + /// This takes a string that is representative of a filename and tries to create a path that can be considered the + /// 'canonical' path. path on drives that are mapped as remote shares are rewritten as their \\server\share\path + /// + /// + public static string CanonicalizePath(this string path, bool isPotentiallyRelativePath) { + Uri pathUri = null; + try { + pathUri = new Uri(path); + if (!pathUri.IsFile) { + // perhaps try getting the fullpath + try { + pathUri = new Uri(Path.GetFullPath(path)); + } catch { + return null; + } + } + + // is this a unc path? + if (string.IsNullOrWhiteSpace(pathUri.Host)) { + // no, this is a drive:\path path + // use API to resolve out the drive letter to see if it is a remote + var drive = pathUri.Segments[1].Replace('/', '\\'); // the zero segment is always just '/' + + var sb = new StringBuilder(512); + var size = sb.Capacity; + + //TODO + // var error = NativeMethods.WNetGetConnection(drive, sb, ref size); + //if (error == 0) { + // if (pathUri.Segments.Length > 2) { + // return pathUri.Segments.Skip(2).Aggregate(sb.ToString().Trim(), (current, item) => current + item); + // } + // } + } + // not a remote (or resovably-remote) path or + // it is already a path that is in it's correct form (via localpath) + return pathUri.LocalPath; + } catch (UriFormatException) { + // we could try to see if it is a relative path... + if (isPotentiallyRelativePath) { + return CanonicalizePath(Path.GetFullPath(path), false); + } + return null; + } + } + + public static byte[] ReadBytes(this string path, int maxLength) { + if (path.FileExists()) { + try { + var buffer = new byte[Math.Min(new FileInfo(path).Length, maxLength)]; + using (var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read)) { + file.Read(buffer, 0, buffer.Length); + } + + return buffer; + } catch { + // not openable. whatever. + } + } + return new byte[0]; + } + + public static bool FileExists(this string path) { + if (!string.IsNullOrWhiteSpace(path)) { + try { + return File.Exists(CanonicalizePath(path, true)); + } catch { + } + } + return false; + } + + public static bool DirectoryExists(this string path) { + if (!string.IsNullOrWhiteSpace(path)) { + try { + return Directory.Exists(CanonicalizePath(path, true)); + } catch { + } + } + return false; + } + + public static string MakeSafeFileName(this string input) { + return new Regex(@"-+").Replace(new Regex(@"[^\d\w\[\]_\-\.\ ]").Replace(input, "-"), "-").Replace(" ", ""); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/MutableEnumerable.cs b/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/MutableEnumerable.cs new file mode 100644 index 000000000..695cc2a0c --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/MutableEnumerable.cs @@ -0,0 +1,99 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Provider.Utility { + using System.Collections; + using System.Collections.Generic; + + internal abstract class MutableEnumerable : IEnumerable { + private List _list; + + protected List List { + get { + if (_list == null) { + lock (this) { + if (_list == null) { + _list = new List(); + } + } + } + return _list; + } + } + + public IEnumerable> GetEnumerators(int copies) { + for (var i = 0; i < copies; i++) { + yield return GetEnumerator(); + } + } + + protected abstract bool ItemExists(int index); + + internal class Enumerator : IEnumerator { + private MutableEnumerable _collection; + private int _index = -1; + + internal Enumerator(MutableEnumerable collection) { + _collection = collection; + } + + #region IEnumerator Members + + public TT Current { + get { + return _collection.List[_index]; + } + } + + public void Dispose() { + _collection = null; + } + + object IEnumerator.Current { + get { + return Current; + } + } + + public bool MoveNext() { + _index++; + return _collection.ItemExists(_index); + } + + public void Reset() { + _index = -1; + } + + public IEnumerator Clone() { + return new Enumerator(_collection) { + _index = _index + }; + } + + #endregion + } + + #region IEnumerable Members + + public IEnumerator GetEnumerator() { + return new Enumerator(this); + } + + IEnumerator IEnumerable.GetEnumerator() { + return GetEnumerator(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/ReEnumerable.cs b/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/ReEnumerable.cs new file mode 100644 index 000000000..28aae65eb --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/inbox/Common/Extensions/ReEnumerable.cs @@ -0,0 +1,77 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PackageManagement.Provider.Utility { + using System.Collections.Generic; + using System.Linq; + + internal class ReEnumerable : MutableEnumerable { + private IEnumerator _sourceIterator; + private readonly IEnumerable _source; + + public ReEnumerable(IEnumerable source) { + _source = source ?? new T[0]; + } + + public ReEnumerable(IEnumerator sourceIterator) { + _source = null; + _sourceIterator = sourceIterator; + } + + public T this[int index] { + get { + if (ItemExists(index)) { + return List[index]; + } + return default(T); + } + } + + public int Count { + get { + return this.Count(); + } + } + + protected override bool ItemExists(int index) { + if (index < List.Count) { + return true; + } + + lock (this) { + if (_sourceIterator == null) { + _sourceIterator = _source.GetEnumerator(); + } + + try { + while (_sourceIterator.MoveNext()) { + List.Add(_sourceIterator.Current); + if (index < List.Count) { + return true; + } + } + } catch { + // if the _sourceIterator is cancelled + // then MoveNext() will throw; that's ok + // that just means we're done + } + } + return false; + } + + public MutableEnumerable Concat(IEnumerable additionalItems) { + return Enumerable.Concat(this, additionalItems).ReEnumerable(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/providers/inbox/Common/Utility/FileUtility.cs b/src/Microsoft.PackageManagement/providers/inbox/Common/Utility/FileUtility.cs new file mode 100644 index 000000000..bc1eaa92f --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/inbox/Common/Utility/FileUtility.cs @@ -0,0 +1,256 @@ +namespace Microsoft.PackageManagement.Provider.Utility +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Text.RegularExpressions; + using System.Threading; + + public static class FileUtility + { + public static string GetTempFileFullPath(string fileExtension) + { + + if (string.IsNullOrWhiteSpace(fileExtension)) + { + return fileExtension; + } + + string tempFolder = Path.GetTempPath(); + + string randomFileName = Path.GetRandomFileName(); + + //get rid of the file extension + randomFileName = Path.GetFileNameWithoutExtension(randomFileName) + fileExtension; + + string file = Path.Combine(tempFolder, randomFileName); + + if (File.Exists(file)) + { + //try it again if the generated file already exists + file = GetTempFileFullPath(fileExtension); + } + + return file; + } + + public static string MakePackageFileName(bool excludeVersion, string packageName, string version, string fileExtension) + { + string fileName = (excludeVersion) ? packageName : (packageName + "." + version); + return fileName + fileExtension; + } + + public static string MakePackageDirectoryName(bool excludeVersion, string destinationPath, string packageName, string version) + { + string baseDir = Path.Combine(destinationPath, packageName); + + return (excludeVersion) ? baseDir : (baseDir + "." + version); + } + + public static void CopyDirectory(string source, string dest, bool copySubDirs) + { + // Get the subdirectories for the specified directory. + DirectoryInfo dir = new DirectoryInfo(source); + + if (!dir.Exists) + { + throw new DirectoryNotFoundException(string.Format(CultureInfo.InvariantCulture, "Source directory '{0}' does not exist or could not be found.", source)); + } + + // Some packages have directories with spaces. However it shows as $20, e.g. Install-Module -name xHyper-VBackup. + // It has something like Content\Deployment\Module%20References\..., with that, the PowerShellGet provider won't be able to handle it. + // Add the following code to unescape percent-encoding characters + string newdest = Uri.UnescapeDataString(dest); + + // If the destination directory doesn't exist, create it. + if (!Directory.Exists(newdest)) + { + Directory.CreateDirectory(newdest); + } + + // Get the files in the directory and copy them to the new location. + FileInfo[] files = dir.GetFiles(); + foreach (FileInfo file in files) + { + string tempPath = Path.Combine(newdest, file.Name); + // unescape special characters like %20 + tempPath = Uri.UnescapeDataString(tempPath); + + file.CopyTo(tempPath, true /*overwrite*/); + } + + // If copying subdirectories, copy them and their contents to new location. + if (copySubDirs) + { + DirectoryInfo[] sourceDirs = dir.GetDirectories(); + + foreach (DirectoryInfo subdir in sourceDirs) + { + string temppath = Path.Combine(newdest, subdir.Name); + CopyDirectory(subdir.FullName, temppath, true); + } + } + } + + public static void DeleteDirectory(string fullPath, bool recursive, bool isThrow) + { + DoSafeAction(() => DeleteDirectory(fullPath, recursive), isThrow); + } + + public static void DeleteFile(string fullPath, bool isThrow) + { + DoSafeAction(() => DeleteFile(fullPath), isThrow); + } + + public static IEnumerable GetFiles(string fullPath, string filter, bool recursive) + { + fullPath = PathUtility.EnsureTrailingSlash(fullPath); + if (String.IsNullOrWhiteSpace(filter)) + { + filter = "*.*"; + } + try + { + if (!Directory.Exists(fullPath)) + { + return Enumerable.Empty(); + } + return Directory.EnumerateFiles(fullPath, filter, recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); + // .Select(MakeRelativePath); + } + catch (UnauthorizedAccessException) + { + + } + catch (DirectoryNotFoundException) + { + + } + + return Enumerable.Empty(); + } + + public static IEnumerable GetDirectories(string fullPath) + { + try + { + fullPath = PathUtility.EnsureTrailingSlash(fullPath); + if (!Directory.Exists(fullPath)) + { + return Enumerable.Empty(); + } + + return Directory.EnumerateDirectories(fullPath); + } + catch (UnauthorizedAccessException) + { + + } + catch (DirectoryNotFoundException) + { + + } + + return Enumerable.Empty(); + } + + public static DateTimeOffset GetLastModified(string fullPath) + { + if (File.Exists(fullPath)) + { + return File.GetLastWriteTime(fullPath); + } + return Directory.GetLastWriteTime(fullPath); + } + + private static void DeleteFile(string path) + { + if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) + { + return; + } + + try + { + //sometimes there are difficulties to delete readonly files + MakeFileWritable(path); + File.Delete(path); + } + catch (FileNotFoundException) + { + } + } + + private static void MakeFileWritable(string path) + { + FileAttributes attributes = File.GetAttributes(path); + if (attributes.HasFlag(FileAttributes.ReadOnly)) + { + File.SetAttributes(path, attributes & ~FileAttributes.ReadOnly); + } + } + + private static void DeleteDirectory(string fullPath, bool recursive) + { + if (string.IsNullOrWhiteSpace(fullPath) || !Directory.Exists(fullPath)) + { + return; + } + + try + { + //path = GetFullPath(path); + Directory.Delete(fullPath, recursive); + + // The directory is not guaranteed to be gone since there could be + // other open handles. Wait, up to half a second, until the directory is gone. + for (int i = 0; Directory.Exists(fullPath) && i < 5; ++i) + { + Thread.Sleep(100); + } + } + catch (DirectoryNotFoundException) + { + } + } + + // [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to log an exception as a warning and move on")] + private static void DoSafeAction(Action action, bool isThrow) + { + try + { + Attempt(action); + } + catch (Exception e) + { + if (isThrow) + { + throw new Exception(e.Message); + } + } + } + + private static void Attempt(Action action, int retries = 5, int delayBeforeRetry = 100) + { + while (retries > 0) + { + try + { + action(); + break; + } + catch + { + retries--; + if (retries == 0) + { + throw; + } + } + Thread.Sleep(delayBeforeRetry); + } + } + } +} diff --git a/src/Microsoft.PackageManagement/providers/inbox/Common/Utility/PathUtility.cs b/src/Microsoft.PackageManagement/providers/inbox/Common/Utility/PathUtility.cs new file mode 100644 index 000000000..6f4d357ca --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/inbox/Common/Utility/PathUtility.cs @@ -0,0 +1,253 @@ + +using System.Security; + +namespace Microsoft.PackageManagement.Provider.Utility +{ + using System; + using System.IO; + using System.Net; + using System.Net.Http; + using System.Threading; + using System.Threading.Tasks; + using System.Runtime.InteropServices; + using System.Globalization; + + internal static class PathUtility + { + + internal static string EnsureTrailingSlash(string path) + { + //The value of DirectorySeparatorChar is a slash ("/") on UNIX, and a backslash ("\") on the Windows and Macintosh. + return EnsureTrailingCharacter(path, Path.DirectorySeparatorChar); + } + + private static string EnsureTrailingCharacter(string path, char trailingCharacter) + { + if (path == null) + { + throw new ArgumentNullException("path"); + } + + // if the path is empty, we want to return the original string instead of a single trailing character. + if (path.Length == 0 || path[path.Length - 1] == trailingCharacter) + { + return path; + } + + return path + trailingCharacter; + } + + internal static HttpResponseMessage GetHttpResponse(HttpClient httpClient, string query, Func isCanceled, ActionlogRetry, Actionverbose, Actiondebug) + { + // try downloading for 3 times + int remainingTry = 3; + object timerLock = new object(); + Timer timer = null; + CancellationTokenSource cts = null; + bool cleanUp = true; + + Action cleanUpAction = () => + { + lock (timerLock) + { + // check whether clean up is already done before or not + if (!cleanUp) + { + try + { + if (timer != null) + { + // stop timer + timer.Change(Timeout.Infinite, Timeout.Infinite); + // dispose it + timer.Dispose(); + } + + // dispose the token + if (cts != null) + { + cts.Cancel(); + cts.Dispose(); + } + } + catch { } + + cleanUp = true; + } + } + }; + + while (remainingTry > 0) + { + // if user cancel the request, no need to do anything + if (isCanceled()) + { + break; + } + + try + { + // decrease try by 1 + remainingTry -= 1; + + // create new timer and cancellation token source + lock (timerLock) + { + // check every second to see whether request is cancelled + timer = new Timer(_ => + { + if (isCanceled()) + { + cleanUpAction(); + } + }, null, 500, 1000); + + cts = new CancellationTokenSource(); + + cleanUp = false; + } + + Task task = httpClient.GetAsync(query, cts.Token); + + // start the task + task.Wait(); + + if (task.IsCompleted && task is Task) + { + var result = (task as Task).Result; + + // if success, returns result + if (result.IsSuccessStatusCode) + { + return result; + } + + // otherwise, we have to retry again + } + + // if request is canceled, don't retry + if (isCanceled()) + { + break; + } + + logRetry(query, remainingTry); + } + catch (Exception ex) + { + if (ex is AggregateException) + { + (ex as AggregateException).Handle(singleException => + { + // report each of the exception + verbose(singleException.Message); + debug(singleException.StackTrace); + return true; + }); + } + else + { + // single exception, just report the message and stacktrace + verbose(ex.Message); + debug(ex.StackTrace); + } + + // if there is exception, we will retry too + logRetry(query, remainingTry); + } + finally + { + cleanUpAction(); + } + } + + return null; + } + + + internal static string UriCombine(string query, string append) + { + if (String.IsNullOrWhiteSpace(query)) return append; + if (String.IsNullOrWhiteSpace(append)) return query; + + return query.TrimEnd('/') + "/" + append.TrimStart('/'); + } + + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "It's used in Nano")] + internal static string SecureStringToString(SecureString secure) + { + IntPtr value = IntPtr.Zero; + try + { +#if !CORECLR + value = Marshal.SecureStringToCoTaskMemUnicode(secure); +#else + value = SecureStringMarshal.SecureStringToCoTaskMemUnicode(secure); +#endif + return Marshal.PtrToStringUni(value); + } + finally + { +#if !CORECLR + Marshal.ZeroFreeGlobalAllocUnicode(value); +#else + SecureStringMarshal.ZeroFreeCoTaskMemUnicode(value); +#endif + } + } + + internal static NetworkCredential GetNetworkCredential(string username, SecureString password) + { + // if request has username and password, use that + if (!string.IsNullOrWhiteSpace(username) && password != null) + { +#if CORECLR + // networkcredential class on coreclr does not accept securestring so we have to convert + return new NetworkCredential(username, SecureStringToString(password)); +#else + return new NetworkCredential(username, password); +#endif + } + + // if no user name and password, returns null + return null; + } + + internal static HttpClient GetHttpClientHelper(string username, SecureString password, IWebProxy webProxy) + { + var clientHandler = new HttpClientHandler(); + + var networkCredential = GetNetworkCredential(username, password); + + // if we are given a network credential, use that + if (networkCredential != null) + { + // else use the one given to us + clientHandler.Credentials = networkCredential; + clientHandler.PreAuthenticate = true; + } + else + { + clientHandler.UseDefaultCredentials = true; + } + + // do not need to set proxy of httpClient or httpClientHandler because it will use system proxy setting by default + // discussion here (https://github.com/dotnet/corefx/issues/7037) + + if (webProxy != null) + { + // if webproxy is not null, use that + clientHandler.Proxy = webProxy; + } + + var httpClient = new HttpClient(clientHandler); + + // Mozilla/5.0 is the general token that says the browser is Mozilla compatible, and is common to almost every browser today. + httpClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "Mozilla/5.0 NuGet"); + + return httpClient; + } + + } +} \ No newline at end of file diff --git a/src/Microsoft.PackageManagement/providers/inbox/Common/Utility/XmlUtility.cs b/src/Microsoft.PackageManagement/providers/inbox/Common/Utility/XmlUtility.cs new file mode 100644 index 000000000..bac0822f0 --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/inbox/Common/Utility/XmlUtility.cs @@ -0,0 +1,36 @@ +namespace Microsoft.PackageManagement.Provider.Utility +{ + using System.IO; + using System.Xml; + using System.Xml.Linq; + + public static class XmlUtility + { + public static XDocument LoadSafe(string filePath) + { + var settings = CreateSafeSettings(); + using (var reader = XmlReader.Create(filePath, settings)) + { + return XDocument.Load(reader); + } + } + + public static XDocument LoadSafe(Stream input, bool ignoreWhiteSpace) + { + var settings = CreateSafeSettings(ignoreWhiteSpace); + var reader = XmlReader.Create(input, settings); + return XDocument.Load(reader); + } + + private static XmlReaderSettings CreateSafeSettings(bool ignoreWhiteSpace = false) + { + var safeSettings = new XmlReaderSettings + { + DtdProcessing = DtdProcessing.Prohibit, + IgnoreWhitespace = ignoreWhiteSpace + }; + + return safeSettings; + } + } +} diff --git a/src/Microsoft.PackageManagement/providers/inbox/Common/Version/DependencyVersion.cs b/src/Microsoft.PackageManagement/providers/inbox/Common/Version/DependencyVersion.cs new file mode 100644 index 000000000..16172dd82 --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/inbox/Common/Version/DependencyVersion.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Globalization; + +namespace Microsoft.PackageManagement.Provider.Utility +{ + /// + /// Represents dependency version returned by nuget api call + /// + public class DependencyVersion + { + public SemanticVersion MinVersion { get; set; } + /// + /// True if the version we are looking for includes min version + /// + + public bool IsMinInclusive { get; set; } + public SemanticVersion MaxVersion { get; set; } + /// + /// True if the version we are looking for includes the max version + /// + public bool IsMaxInclusive { get; set; } + + /// + /// Parse and return a dependency version + /// The version string is either a simple version or an arithmetic range + /// e.g. + /// 1.0 --> 1.0 ≤ x + /// (,1.0] --> x ≤ 1.0 + /// (,1.0) --> x lt 1.0 + /// [1.0] --> x == 1.0 + /// (1.0,) --> 1.0 lt x + /// (1.0, 2.0) --> 1.0 lt x lt 2.0 + /// [1.0, 2.0] --> 1.0 ≤ x ≤ 2.0 + /// + /// + /// + /// + public static DependencyVersion ParseDependencyVersion(string value) + { + var depVers = new DependencyVersion(); + + if (String.IsNullOrWhiteSpace(value)) + { + return depVers; + } + + value = value.Trim(); + + char first = value.First(); + char last = value.Last(); + + if (first != '(' && first != '[' && last != ']' && last != ')') + { + // Stand alone version + depVers.IsMinInclusive = true; + depVers.MinVersion = new SemanticVersion(value); + return depVers; + } + + // value must have length greater than 3 + if (value.Length < 3) + { + return depVers; + } + + // The first character must be [ or ( + switch (value.First()) + { + case '[': + depVers.IsMinInclusive = true; + break; + case '(': + depVers.IsMinInclusive = false; + break; + default: + // If not, return without setting anything + return depVers; + } + + // The last character must be ] or ) + switch (value.Last()) + { + case ']': + depVers.IsMaxInclusive = true; + break; + case ')': + depVers.IsMaxInclusive = false; + break; + default: + // If not, return without setting anything + return depVers; + } + + // Get rid of the two brackets + value = value.Substring(1, value.Length - 2); + + // Split by comma, and make sure we don't get more than two pieces + string[] parts = value.Split(','); + + // Wrong format if we have more than 2 parts or all the parts are empty + if (parts.Length > 2 || parts.All(String.IsNullOrEmpty)) + { + return depVers; + } + + // First part is min + string minVersionString = parts[0]; + + // If there is only 1 part then first part will also be max + string maxVersionString = (parts.Length == 2) ? parts[1] : parts[0]; + + // Get min version if we have it + if (!String.IsNullOrWhiteSpace(minVersionString)) + { + depVers.MinVersion = new SemanticVersion(minVersionString); + } + + // Get max version if we have it + if (!String.IsNullOrWhiteSpace(maxVersionString)) + { + depVers.MaxVersion = new SemanticVersion(maxVersionString); + } + + return depVers; + } + + public override string ToString() + { + // Returns nothing if no min or max + if (MinVersion == null && MaxVersion == null) + { + return null; + } + + // If we have min and minInclusive but no max, then return min string + if (MinVersion != null && IsMinInclusive && MaxVersion == null && !IsMaxInclusive) + { + return MinVersion.ToString(); + } + + // MinVersion and MaxVersion is the same and both inclusives then return the value + if (MinVersion == MaxVersion && IsMinInclusive && IsMaxInclusive) + { + return String.Format(CultureInfo.InvariantCulture, "[{0}]", MinVersion); + } + + char lhs = IsMinInclusive ? '[' : '('; + char rhs = IsMaxInclusive ? ']' : ')'; + + return String.Format(CultureInfo.InvariantCulture, "{0}{1}, {2}{3}", lhs, MinVersion, MaxVersion, rhs); + } + + } +} diff --git a/src/Microsoft.PackageManagement/providers/inbox/Common/Version/SemanticVersion.cs b/src/Microsoft.PackageManagement/providers/inbox/Common/Version/SemanticVersion.cs new file mode 100644 index 000000000..dc48cf249 --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/inbox/Common/Version/SemanticVersion.cs @@ -0,0 +1,332 @@ +using MMicrosoft.PackageManagement.Provider.Utility; + +namespace Microsoft.PackageManagement.Provider.Utility +{ + using System; + using System.ComponentModel; + using System.Globalization; + using System.Text.RegularExpressions; + + /// + /// A hybrid implementation of SemVer that supports semantic versioning as described at http://semver.org while not strictly enforcing it to + /// allow older 4-digit versioning schemes to continue working. + /// + /// Example: EntityFramework 6.1.3-beta1 + /// Version: 6.1.3 + /// SpecialVersion: -beta1 + /// OrginalString: 6.1.3-beta1 + /// + /// + [TypeConverter(typeof(SemanticVersionTypeConverter))] + public sealed class SemanticVersion : IComparable, IComparable, IEquatable + { + private const RegexOptions _flags = RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture; + internal const string AllowFourPartsVersion = @"(?\d+(\s*\.\s*\d+){0,3})"; + internal const string ThreePartsVersion = @"(?\d+(\.\d+){2})"; + // the pre release regex is of the form - where version is set of identifier delimited by ".". Each identifer can be any characters in [A-z0-9a-z-] + internal const string ReleasePattern = @"(?-[A-Z0-9a-z\-]+(\.[A-Z0-9a-z\-]+)*)?"; + // The build regex is of the same form except with a + instead of - + internal const string BuildPattern = @"(?\+[A-Z0-9a-z\-]+(\.[A-Z0-9a-z\-]+)*)?"; + internal static string SemanticVersionPattern = String.Concat("^", AllowFourPartsVersion, ReleasePattern, BuildPattern, "$"); + + private static readonly Regex _semanticVersionRegex = new Regex(SemanticVersionPattern, _flags); + private static readonly Regex _strictSemanticVersionRegex = new Regex(@"^(?\d+(\.\d+){2})(?-[A-Z0-9a-z\-]+(\.[A-Z0-9a-z\-]+)*)?(?\+[A-Z0-9a-z\-]+(\.[A-Z0-9a-z\-]+)*)?$", _flags); + private readonly string _originalString; + + public SemanticVersion(string version) + : this(Parse(version)) + { + // The constructor normalizes the version string so that it we do not need to normalize it every time we need to operate on it. + // The original string represents the original form in which the version is represented to be used when printing. + _originalString = version; + } + + public SemanticVersion(int major, int minor, int build, int revision) + : this(new Version(major, minor, build, revision)) + { + } + + public SemanticVersion(int major, int minor, int build, string specialVersion) + : this(new Version(major, minor, build), specialVersion) + { + } + + public SemanticVersion(Version version) + : this(version, String.Empty) + { + } + + public SemanticVersion(Version version, string specialVersion) + : this(version, specialVersion, null) + { + } + + private SemanticVersion(Version version, string specialVersion, string originalString) + { + if (version == null) + { + throw new ArgumentNullException("version"); + } + Version = NormalizeVersionValue(version); + SpecialVersion = specialVersion ?? String.Empty; + _originalString = String.IsNullOrWhiteSpace(originalString) ? (version + (!String.IsNullOrWhiteSpace(specialVersion) ? '-' + specialVersion : null)) : originalString; + } + + public SemanticVersion(SemanticVersion semVer) + { + if (semVer == null) { + throw new ArgumentNullException("semVer"); + } + _originalString = semVer.ToString(); + Version = semVer.Version; + SpecialVersion = semVer.SpecialVersion; + } + + /// + /// Gets the normalized version portion. + /// + public Version Version + { + get; + private set; + } + + /// + /// Gets the optional special version. + /// + public string SpecialVersion + { + get; + private set; + } + + /// + /// Get an array of the major, minor, build, and revision. + /// + /// + public string[] GetOriginalVersionComponents() + { + if (!String.IsNullOrWhiteSpace(_originalString)) + { + string original; + + // search the start of the SpecialVersion part, if any + int dashIndex = _originalString.IndexOf('-'); + if (dashIndex != -1) + { + // remove the SpecialVersion part + original = _originalString.Substring(0, dashIndex); + } + else + { + original = _originalString; + } + + return SplitAndPadVersionString(original); + } + else + { + return SplitAndPadVersionString(Version.ToString()); + } + } + + private static string[] SplitAndPadVersionString(string version) + { + string[] a = version.Split('.'); + if (a.Length == 4) + { + return a; + } + else + { + // if 'a' has less than 4 elements, we pad the '0' at the end + // to make it 4. + var b = new string[4] { "0", "0", "0", "0" }; + Array.Copy(a, 0, b, 0, a.Length); + return b; + } + } + + /// + /// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an optional special version. + /// + public static SemanticVersion Parse(string version) + { + if (String.IsNullOrWhiteSpace(version)) + { + throw new ArgumentNullException("version"); + } + + SemanticVersion semVer; + if (!TryParse(version, out semVer)) + { + return null; + } + + return semVer; + } + + /// + /// Parses a version string using loose semantic versioning rules that allows 2-4 version components followed by an optional special version. + /// + public static bool TryParse(string version, out SemanticVersion value) + { + return TryParseInternal(version, _semanticVersionRegex, out value); + } + + /// + /// Parses a version string using strict semantic versioning rules that allows exactly 3 components and an optional special version. + /// + public static bool TryParseStrict(string version, out SemanticVersion value) + { + return TryParseInternal(version, _strictSemanticVersionRegex, out value); + } + + private static bool TryParseInternal(string version, Regex regex, out SemanticVersion semVer) + { + semVer = null; + if (String.IsNullOrWhiteSpace(version)) + { + return false; + } + + var match = regex.Match(version.Trim()); + Version versionValue; + if (!match.Success || !Version.TryParse(match.Groups["Version"].Value, out versionValue)) + { + return false; + } + + semVer = new SemanticVersion(NormalizeVersionValue(versionValue), match.Groups["Release"].Value.TrimStart('-'), version.Replace(" ", "")); + return true; + } + + private static Version NormalizeVersionValue(Version version) + { + return new Version(version.Major, + version.Minor, + Math.Max(version.Build, 0), + Math.Max(version.Revision, 0)); + } + + public int CompareTo(object obj) + { + if (ReferenceEquals(obj, null)) + { + return 1; + } + SemanticVersion other = obj as SemanticVersion; + if (other == null) + { + throw new ArgumentNullException("obj"); + } + return CompareTo(other); + } + + public int CompareTo(SemanticVersion other) + { + if (other == null) + { + return 1; + } + + int result = Version.CompareTo(other.Version); + + if (result != 0) + { + return result; + } + + bool empty = String.IsNullOrWhiteSpace(SpecialVersion); + bool otherEmpty = String.IsNullOrWhiteSpace(other.SpecialVersion); + if (empty && otherEmpty) + { + return 0; + } + else if (empty) + { + return 1; + } + else if (otherEmpty) + { + return -1; + } + return StringComparer.OrdinalIgnoreCase.Compare(SpecialVersion, other.SpecialVersion); + } + + public static bool operator ==(SemanticVersion version1, SemanticVersion version2) + { + if (ReferenceEquals(version1, null)) + { + return ReferenceEquals(version2, null); + } + return version1.Equals(version2); + } + + public static bool operator !=(SemanticVersion version1, SemanticVersion version2) + { + return !(version1 == version2); + } + + public static bool operator <(SemanticVersion version1, SemanticVersion version2) + { + if (version1 == null) + { + throw new ArgumentNullException("version1"); + } + return version1.CompareTo(version2) < 0; + } + + public static bool operator <=(SemanticVersion version1, SemanticVersion version2) + { + return (version1 == version2) || (version1 < version2); + } + + public static bool operator >(SemanticVersion version1, SemanticVersion version2) + { + if (version1 == null) + { + throw new ArgumentNullException("version1"); + } + return version2 < version1; + } + + public static bool operator >=(SemanticVersion version1, SemanticVersion version2) + { + return (version1 == version2) || (version1 > version2); + } + + public override string ToString() + { + return _originalString; + } + + public bool Equals(SemanticVersion other) + { + if (other == null) + { + return ReferenceEquals(Version, null); + } + + return Version.Equals(other.Version) && + SpecialVersion.Equals(other.SpecialVersion, StringComparison.OrdinalIgnoreCase); + } + + public override bool Equals(object obj) + { + SemanticVersion semVer = obj as SemanticVersion; + return !ReferenceEquals(null, semVer) && Equals(semVer); + } + + public override int GetHashCode() + { + int hashCode = Version.GetHashCode(); + if (SpecialVersion != null) + { + hashCode = hashCode * 4567 + SpecialVersion.GetHashCode(); + } + + return hashCode; + } + } +} diff --git a/src/Microsoft.PackageManagement/providers/inbox/Common/Version/SemanticVersionTypeConverter.cs b/src/Microsoft.PackageManagement/providers/inbox/Common/Version/SemanticVersionTypeConverter.cs new file mode 100644 index 000000000..00c494d95 --- /dev/null +++ b/src/Microsoft.PackageManagement/providers/inbox/Common/Version/SemanticVersionTypeConverter.cs @@ -0,0 +1,30 @@ +using Microsoft.PackageManagement.Provider.Utility; + +namespace MMicrosoft.PackageManagement.Provider.Utility +{ + using System; + using System.ComponentModel; + using System.Globalization; + + /// + /// Convert String to SemanticVersion type + /// + public class SemanticVersionTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + var stringValue = value as string; + SemanticVersion semVer; + if (stringValue != null && SemanticVersion.TryParse(stringValue, out semVer)) + { + return semVer; + } + return null; + } + } +} diff --git a/src/Microsoft.PackageManagement/resources/Microsoft.PackageManagement.Resources.Messages.resx b/src/Microsoft.PackageManagement/resources/Microsoft.PackageManagement.Resources.Messages.resx new file mode 100644 index 000000000..09e779ae7 --- /dev/null +++ b/src/Microsoft.PackageManagement/resources/Microsoft.PackageManagement.Resources.Messages.resx @@ -0,0 +1,360 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Unable to create specified folder '{0}'. + 0 - folder location + + + Dependent Package '{0}' failed to install. + 0 - package that failed install. + + + Destination Path not set. + + + Failed to bootstrap provider '{0}'. + 0 - the provider name + + + File from '{0}' failed signature validation. + 0 href to file location + + + The expected hash '{0}' is not equal to the computed hash '{1}' from the file to be installed. + 0 hash, 1 - hash 2 - filepath + + + The filename specified is not valid. + + + The parameter '{0}' is required for this operation. + 0 parameter name + + + The package source '{0}' already exists with that provider. + 0 - package source name or location + + + A protocol handler for scheme '{0}' is not available. + 0 protocol scheme + + + Specified version range is invalid. Minimum Version:{0} MaximumVersion:{1}. + 0 protocol scheme + + + Provider '{0}' failed to load. + + + Unable to download the list of available providers. Check your internet connection. + + + Removal of the environment variable '{0}' requires elevation. + 0 - environment variable name + + + A protocol handler for scheme '{0}' is not available. + 0 protocol scheme + + + Source location '{0}' is not valid for this provider. + Indicates the user has passed in a location (url, path, etc) that the provider can not resolve. + + + Unable to copy file to destination '{0}'. + 0 - destination path + + + Unable to create shortcut for file '{0}' because it does not exist. + 0 file that is missing + + + Unable to download from URI '{0}' to '{1}'. + 0 source uri, 1 destination file name + + + The package '{0}' failed to install. Please look at '{1}' file for details. + 0 package id, 1 path to error log file + + + The package '{0}' failed to install. + 0 package id + + + Unable to overwrite existing file '{0}'. + 0 filename + + + Unable to remove existing file '{0}'. + 0 filename + + + Unable to resolve package dependency -- package '{0}' not found. + 0 - package that can't be resolved. + + + Unable to resolve package reference '{0}'. + 0 encoded package reference. + + + Unable to resolve package source '{0}'. + 0 package source or location + + + Unable to uninstall package. + + + Unknown Folder Id '{0}'. + 0 - folder id + + + Unable to find package provider '{0}'. + {0} -provider name + + + The archive file type is not supported. + + + The provider type is not supported. + + + Uri Scheme '{0}' is not supported. + + + User declined to install untrusted package '{0}'. + 0 -package name + + + PowerShell meta provider initialization failed. + + + The provider '{0}' is ignored because the other provider with the same name has already been imported from path '{1}'. + 0-Provider name or path, {1} - provider path + + + The specified file '{0}' is not found. + 0 - provider file path + + + The specified provider '{0}' was not loaded because no valid type was found. The supported file types are '{1}'. + 0- provider path or name. 1 - file extension. .exe, .dll, etc. + + + Failed to import the specified provider '{0}'. Please check if it is a valid PackageManagement provider. + + + Loading an assembly '{0}'. + + + Loading a PowerShell module '{0}'. + + + The provider '{0}' has already been imported. + + + The provider {0} is successfully loaded. + + + Skipping previously processed provider '{0}'. + 0- provider name + + + The provider '{0}' has already been imported. Trying to import it again. + + + Skipping previously processed assembly: {0}. + + + Unable to import provider '{0}' under the current file path. The known provider search paths are '{1}.' + 0 - provider path or name. 1 - path + + + The requiredVersion, minimumVersion or maximumVersion property applies to PowerShell modules. For the assembly providers, it is ignored. + 0 - provider name or path + + + Importing package provider '{0}'. + 0 - package provider name or path + + + Failed to re-import the provider '{0}'. + + + Unable to find package provider: {0}. + {0} - version number + + + The provider is loaded from path: '{0}'. + 0- file path + + + Version {0} of the Package Provider {1} is already loaded. Since this is an assembly-based provider, we cannot load another version. + + + The specified assembly '{0}' in \ProviderAssemblies, should be installed in ProviderAssemblies\ProviderName\Version. Either delete the provider assembly from \ProviderAssemblies, or create \ProviderAssemblies\ProvidernName\Version, and then move the assembly there. + + + Multiple assemblies are found under path '{0}'. Please install a single provider assembly under each version folder. + 0 - file path + + + No match was found for the specified search criteria and provider name '{0}'. Try 'Get-PackageProvider -ListAvailable' to see if the provider exists on the system. + 0 - provider name + + + Wildcards in the '{0}' is not supported. Please specify a full provider name. + 0 - cmdlet name + + + Administrator rights are required to install packages in '{0}'. Log on to the computer with an account that has Administrator rights, and then try again, or install '{1}' by adding "-Scope CurrentUser" to your command. You can also try running the Windows PowerShell session with elevated rights (Run as Administrator). + + + The format of the specified hash '{0}' is invalid. + 0 - hash value + + + Missing File xml tag. + + + Missing the hash attribute within the File xml tag. + + + The value of the hash xml attribute is empty. + + + The specified hash algorithm '{0}' is not supported. Please try using '{1}' instead. + 0 - hash algorithm + + + Provider '{0}' is not respecting the responsiveness threshold in a timely fashion; Canceling request. + 0 - provider name + + + "Provider '{0}' is not completing the request in the time allowed; Canceling request. + 0 - provider name + + + Xml '{0}' is not a valid SoftwareIdentity swidtag. It needs to use '{1}' namespace. + 0 - the xml; 1 - the xml namespace + + + The swidtag xml returned by the provider is not valid. + + \ No newline at end of file diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/AsyncCmdlet.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/AsyncCmdlet.cs new file mode 100644 index 000000000..5be05e2e1 --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/AsyncCmdlet.cs @@ -0,0 +1,1330 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets { + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Management.Automation; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.PackageManagement.Internal.Utility.Collections; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Internal.Utility.Platform; + using Utility; + + public delegate bool OnMainThread(Func onMainThreadDelegate); + + public abstract class AsyncCmdlet : PSCmdlet, IDynamicParameters, IDisposable { + protected enum AsyncCmdletState { + Unknown, + GenerateParameters, + BeginProcess, + BeginProcessCompleted, + ProcessRecord, + ProcessRecordCompleted, + EndProcess, + EndProcessCompleted, + StopProcess, + StopProcessCompleted + } + + private const BindingFlags BindingFlags = System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public; + private ProgressTracker _activeProgressId; + protected AsyncCmdletState CmdletState = AsyncCmdletState.Unknown; + protected CancellationTokenSource CancellationEvent = new CancellationTokenSource(); + private bool _consumedDynamicParameters; + private RuntimeDefinedParameterDictionary _dynamicParameters; + protected bool ErrorState; + private BlockingCollection> _heldMessages; + private BlockingCollection> _messages; + private int _nextProgressId = 1; + private Stopwatch _stopwatch; + private Dictionary _unboundArguments; + + private readonly SortedSet _errors = new SortedSet(); + private readonly List _progressTrackers = new List(); + private readonly SortedSet _warnings = new SortedSet(); + + private PipelineStoppedException _pipelineStopped; + + private ManualResetEvent ReentrantLock { + get { + // ... + // In the future, I suspect that + // we need to make this 'RunSpace-static' (or 'host-static'?) too + // if the same set of cmdlets ends up loaded in another + // runspace, and we don't want a lock in one to affect + // another. + + return GetType().GetOrAdd(() => new ManualResetEvent(false), "ReentrancyLock"); + } + } + +#if DEBUG + private static object __lock = new object(); + private void Log(string category, string text) { + lock (__lock) { + NativeMethods.OutputDebugString("[Cmdlet:{0}][Thread:{1}][{2}] {3}".format(GetType().Name, Thread.CurrentThread.ManagedThreadId, category, text)); + } + } +#endif + + /// + /// Manages the re-entrancy lock for cmdlets + /// This is abstracted here because (at this point) + /// we need this to be static, but per-cmdlet class. + /// + protected bool IsReentrantLocked { + get { + return ReentrantLock.WaitOne(0); + } + set { + if (value) { + ReentrantLock.Set(); + } else { + ReentrantLock.Reset(); + } + } + } + + protected bool Confirm { + get { + return MyInvocation.BoundParameters.ContainsKey(Constants.Parameters.ConfirmParameter) && (SwitchParameter)MyInvocation.BoundParameters[Constants.Parameters.ConfirmParameter]; + } + } + + public bool WhatIf { + get { + return MyInvocation.BoundParameters.ContainsKey(Constants.Parameters.WhatIfParameter) && (SwitchParameter)MyInvocation.BoundParameters[Constants.Parameters.WhatIfParameter]; + } + } + + protected static bool IsInitialized {get; set;} + + protected bool IsInvocation { + get { + // this seems to be more reliable than checking the Invocation Line. + return MyInvocation != null && MyInvocation.PipelineLength > 0; + } + } + + /// + /// The provider can query to see if the operation has been cancelled. + /// This provides for a gentle way for the caller to notify the callee that + /// they don't want any more results. + /// + /// returns TRUE if the operation has been cancelled. + public bool IsCanceled { + get { + return Stopping || CancellationEvent == null || CancellationEvent.IsCancellationRequested; + } + } + + protected bool HasErrors { + get { + return _errors.Any(); + } + } + + protected Dictionary UnboundArguments { + get { + if (_unboundArguments == null && IsReentrantLocked) { + _unboundArguments = new Dictionary(StringComparer.OrdinalIgnoreCase); + + try { + var context = TryGetProperty(this, "Context"); + var processor = TryGetProperty(context, "CurrentCommandProcessor"); + var parameterBinder = TryGetProperty(processor, "CmdletParameterBinderController"); + var args = TryGetProperty(parameterBinder, "UnboundArguments") as IEnumerable; + + if (args != null) { + var currentParameterName = string.Empty; + int i = 0; + foreach (var arg in args) { + var isParameterName = TryGetProperty(arg, "ParameterNameSpecified"); + if (isParameterName != null && true.Equals(isParameterName)) { + var parameterName = TryGetProperty(arg, "ParameterName"); + + if (parameterName != null) { + currentParameterName = parameterName.ToString(); + + // add it now, just in case it's value isn't set (or it's a switch) + _unboundArguments.AddOrSet(currentParameterName, (object)TryGetProperty(arg, "ArgumentValue")); + continue; + } + } + + // not a parameter name. + // treat as a value + var parameterValue = TryGetProperty(arg, "ArgumentValue"); + + if (string.IsNullOrWhiteSpace(currentParameterName)) { + _unboundArguments.AddOrSet("unbound_" + (i++), parameterValue); + } else { + _unboundArguments.AddOrSet(currentParameterName, parameterValue); + } + + // clear the current parameter name + currentParameterName = null; + } + } + } catch (Exception e) { + e.Dump(); + } + } + return _unboundArguments; + } + } + + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + public virtual bool BeginProcessingAsync() { + return false; + } + + public virtual bool EndProcessingAsync() { + return false; + } + + public virtual bool StopProcessingAsync() { + return false; + } + + public virtual bool ProcessRecordAsync() { + return false; + } + + public string ResolveExistingFilePath(string filePath) { + ProviderInfo providerInfo = null; + var files = GetResolvedProviderPathFromPSPath(filePath, out providerInfo).ToArray(); + switch (files.Length) { + case 0: + // none found + Error(Constants.Errors.FileNotFound, filePath); + break; + + case 1: + if (File.Exists(files[0])) { + return files[0]; + } + Error(Constants.Errors.FileNotFound, filePath); + break; + + default: + Error(Constants.Errors.MoreThanOneFileMatched, files.JoinWithComma(), filePath); + break; + } + return null; + } + + public string ResolveExistingFolderPath(string folderPath) { + return ResolveExistingFolderPath(folderPath, false); + } + + public string ResolveExistingFolderPath(string folderPath, bool isThrow) { + ProviderInfo providerInfo = null; + string[] files = null; + + // we may get an error if the path does not exist. + // we should not error out because the provider may want to create that path. + try + { + files = GetResolvedProviderPathFromPSPath(folderPath, out providerInfo).ToArray(); + } + catch (ItemNotFoundException itemNotFoundEx) + { + if (isThrow) { + throw; + } + // this means path does not exist but the provider may want to create it so we should allow + files = new string[1] { itemNotFoundEx.ItemName }; + } + + switch (files.Length) { + case 0: + // none found + Error(Constants.Errors.FolderNotFound, folderPath); + break; + + case 1: + if (!Directory.Exists(files[0])) { + Verbose(String.Format(CultureInfo.CurrentCulture, Resources.Messages.FolderNotFound, files[0])); + } + return files[0]; + + default: + Error(Constants.Errors.MoreThanOneFolderMatched, files.JoinWithComma(), folderPath); + break; + } + return null; + } + + public string ResolvePath(string path) { + return GetUnresolvedProviderPathFromPSPath(path); + } + + public string DropMsgPrefix(string messageText) { + if (string.IsNullOrWhiteSpace(messageText)) { + return messageText; + } + return messageText.IndexOf("MSG:", StringComparison.OrdinalIgnoreCase) == 0 ? messageText.Substring(4) : messageText; + } + + public bool Warning(string messageText) { + return Warning(messageText, Constants.NoParameters); + } + + internal bool Warning(ErrorMessage message) { + if (message == null) { + throw new ArgumentNullException("message"); + } + return Warning(message.Resource, Constants.NoParameters); + } + + internal bool Warning(ErrorMessage message, params object[] args) { + if (message == null) { + throw new ArgumentNullException("message"); + } + + return Warning(message.Resource, args); + } + + public bool Warning(string messageText, params object[] args) { + if (IsInvocation) { + WriteWarning(FormatMessageString(messageText, args)); + } + // rather than wait on the result of the async WriteVerbose, + // we'll just return the stopping state. + return IsCanceled; + } + + internal bool Error(ErrorMessage errorMessage) { + return Error(errorMessage.Resource, errorMessage.Category.ToString(), null, errorMessage.Resource); + } + + internal bool Error(ErrorMessage errorMessage, params object[] args) { + return Error(errorMessage.Resource, errorMessage.Category.ToString(), null, FormatMessageString(errorMessage.Resource, args)); + } + + public bool Error(string id, string category, string targetObjectValue, string messageText) { + return Error(id, category, targetObjectValue, messageText, Constants.NoParameters); + } + + public bool Error(string id, string category, string targetObjectValue, string messageText, params object[] args) { + if (IsInvocation) { + var errorMessage = FormatMessageString(messageText, args); + + if (!_errors.Contains(errorMessage)) { + + if (!HasErrors) { + // we only *show* the very first error we get. + // any more, we just toss them in the collection and + // maybe we'll worry about them later. + ErrorCategory errorCategory; + if (!Enum.TryParse(category, true, out errorCategory)) { + errorCategory = ErrorCategory.NotSpecified; + } + _errors.Add(errorMessage); + try { +#if DEBUG + Log("ERROR",errorMessage); +#endif + //Replaced Wait() with ConintueWith(Cancel) to avoid hang if the error message comes in too early, e.g. GetDyanamicParameters(), etc. + WriteError(new ErrorRecord(new Exception(errorMessage), DropMsgPrefix(id), errorCategory, string.IsNullOrWhiteSpace(targetObjectValue) ? (object)this : targetObjectValue)).Wait(3000); + } catch { + // this will throw if the provider thread abends before we get back our result. + } + } + } + } + Cancel(); + + // rather than wait on the result of the async'd message, + // we'll just return the stopping state. + return true; + } + + internal bool NonTerminatingError(ErrorMessage errorMessage) + { + return NonTerminatingError(errorMessage.Resource, errorMessage.Category.ToString(), null, errorMessage.Resource); + } + + internal bool NonTerminatingError(ErrorMessage errorMessage, params object[] args) + { + return NonTerminatingError(errorMessage.Resource, errorMessage.Category.ToString(), null, FormatMessageString(errorMessage.Resource, args)); + } + + public bool NonTerminatingError(string id, string category, string targetObjectValue, string messageText) + { + return NonTerminatingError(id, category, targetObjectValue, messageText, Constants.NoParameters); + } + + public bool NonTerminatingError(string id, string category, string targetObjectValue, string messageText, params object[] args) + { + if (IsInvocation) { + var errorMessage = FormatMessageString(messageText, args); + + if (!_errors.Contains(errorMessage)) { + ErrorCategory errorCategory; + if (!Enum.TryParse(category, true, out errorCategory)) { + errorCategory = ErrorCategory.NotSpecified; + } + try { +#if DEBUG + Log("NON TERMINATING ERROR", errorMessage); +#endif + WriteError(new ErrorRecord(new Exception(errorMessage), DropMsgPrefix(id), errorCategory, string.IsNullOrWhiteSpace(targetObjectValue) ? (object)this : targetObjectValue)); + } + catch { + // this will throw if the provider thread abends before we get back our result. + } + } + _errors.Add(errorMessage); + } + // rather than wait on the result of the async'd message, + // we'll just return the stopping state. + return IsCanceled; + } + + public bool Message(string messageText) { + return Message(messageText, Constants.NoParameters); + } + + public bool Message(string messageText, params object[] args) { + // queue the message to run on the main thread. + if (IsInvocation) { + // QueueMessage(() => Host.UI.WriteLine("{0}::{1}".format(code, message.formatWithIEnumerable(objects)))); + // Message is going to go to the verbose channel + // and Verbose will only be output if VeryVerbose is true. +#if DEBUG + Log("Message",FormatMessageString(messageText, args)); +#endif + + WriteVerbose(FormatMessageString(messageText, args)); + } + // rather than wait on the result of the async WriteVerbose, + // we'll just return the stopping state. + return IsCanceled; + } + + public bool Verbose(string messageText) { + return Verbose(messageText, Constants.NoParameters); + } + + public bool Verbose(string messageText, params object[] args) { + if (IsInvocation) { + // Message is going to go to the verbose channel + // and Verbose will only be output if VeryVerbose is true. + WriteVerbose(FormatMessageString(messageText, args)); +#if DEBUG + Log("Verbose",FormatMessageString(messageText, args)); +#endif + } + // rather than wait on the result of the async WriteVerbose, + // we'll just return the stopping state. + return IsCanceled; + } + + public bool Debug(string messageText) { + return Debug(messageText, Constants.NoParameters); + } + + public bool Debug(string messageText, params object[] args) { + if (IsInvocation) { + if (_stopwatch == null) { + _stopwatch = new Stopwatch(); + _stopwatch.Start(); + } + +#if DEBUG + Log("Debug",FormatMessageString(messageText, args)); +#endif + + var task = WriteDebug("{0} {1}".format(_stopwatch.Elapsed, FormatMessageString(messageText, args))); +#if WAIT_FOR_DEBUG + if (_asyncCmdletState > AsyncCmdletState.BeginProcess) { + task.Wait(); + } +#endif + } + + // rather than wait on the result of the async WriteVerbose, + // we'll just return the stopping state. + return IsCanceled; + } + + public int StartProgress(int parentActivityId, string message) { + return StartProgress(parentActivityId, message, Constants.NoParameters); + } + + public int StartProgress(int parentActivityId, string message, params object[] args) { + if (IsInvocation) { + lock (_progressTrackers) { + ProgressTracker parent = null; + + if (parentActivityId <= 0) { + if (_activeProgressId != null) { + parent = _activeProgressId; + } + } else { + parent = _progressTrackers.FirstOrDefault(each => each.Id == parentActivityId); + } + + var p = new ProgressTracker() { + Activity = FormatMessageString(message, args), + Id = _nextProgressId++, + Parent = parent + }; + _activeProgressId = p; + + if (parent != null) { + parent.Children.Add(p); + } + _progressTrackers.Add(p); + + WriteProgress(new ProgressRecord(p.Id, p.Activity, " ") { + PercentComplete = 0, + RecordType = ProgressRecordType.Processing + }); + return p.Id; + } + } + return 0; + } + + /// + /// Write progress using powershell write progress directly + /// + /// Specifies the first line of text in the heading above the status bar. + /// Corresponds to status on write-progress. Specifies the second line of text in the heading above the status bar. This text describes current state of the activity. + /// + /// Corresponds to id on write-progress. Specifies an ID that distinguishes each progress bar from the others. + /// Use this parameter when you are creating more than one progress bar in a single command. + /// If the progress bars do not have different IDs, they are superimposed instead of being displayed in a series. + /// + /// Specifies the percentage of the activity that is completed. Use the value -1 if the percentage complete is unknown or not applicable. + /// Specifies the projected number of seconds remaining until the activity is completed. Use the value -1 if the number of seconds remaining is unknown or not applicable. + /// Specifies the line of text below the progress bar. This text describes the operation that is currently taking place. + /// Identifies the parent activity of the current activity. Use the value -1 if the current activity has no parent activity. + /// Indicates whether the progress bar is visible + /// + public bool Progress(string activity, string messageText, int activityId, int progressPercentage, int secondsRemaining, string currentOperation, int parentActivityId, bool completed) + { + lock (_progressTrackers) + { + if (IsInvocation) + { + // make sure percent complete is not greater than 100 + if (progressPercentage >= 100) + { + progressPercentage = 100; + completed = true; + } + + if (progressPercentage < 0) + { + // set any negative to -1, which means percent complete not applicable + progressPercentage = -1; + } + + if (string.IsNullOrWhiteSpace(messageText)) + { + if (completed) + { + messageText = string.Format(System.Globalization.CultureInfo.CurrentCulture, Resources.Messages.Completed); + } + else + { + messageText = string.Format(System.Globalization.CultureInfo.CurrentCulture, Resources.Messages.Processing); + } + } + + // call the powershell progressrecord directly + WriteProgress(new ProgressRecord(activityId, activity, messageText) + { + PercentComplete = progressPercentage, + RecordType = completed ? ProgressRecordType.Completed : ProgressRecordType.Processing, + SecondsRemaining = secondsRemaining, + CurrentOperation = currentOperation, + ParentActivityId = parentActivityId + }); + } + } + + return IsCanceled; + } + + public bool Progress(int activityId, int progressPercentage, string messageText) { + return Progress(activityId, progressPercentage, messageText, Constants.NoParameters); + } + + public bool Progress(int activityId, int progressPercentage, string messageText, params object[] args) { + lock (_progressTrackers) { + if (IsInvocation) { + var p = _progressTrackers.FirstOrDefault(each => each.Id == activityId); + if (p != null) { + if (progressPercentage >= 100) { + progressPercentage = 100; + } + + WriteProgress(new ProgressRecord(p.Id, p.Activity, FormatMessageString(messageText, args)) { + ParentActivityId = p.Parent != null ? p.Parent.Id : 0, + PercentComplete = progressPercentage, + RecordType = ProgressRecordType.Processing + }); + + if (progressPercentage >= 100) { + return CompleteProgress(activityId, true); + } + } + } + } + // rather than wait on the result of the async WriteVerbose, + // we'll just return the stopping state. + return IsCanceled; + } + + public bool CompleteProgress(int activityId, bool isSuccessful) { + lock (_progressTrackers) { + if (IsInvocation) { + + var p = _progressTrackers.FirstOrDefault(each => each.Id == activityId); + if (p != null) { + // need to clone to avoid collection modification while iterating + var cloneChildrenList = new List(p.Children); + + // complete all of this trackers kids. + foreach (var child in cloneChildrenList) + { + CompleteProgress(child.Id, isSuccessful); + } + + if (p.Parent != null) { + p.Parent.Children.Remove(p); + } + _progressTrackers.Remove(p); + if (_messages == null) { + base.WriteProgress(new ProgressRecord(p.Id, p.Activity, "Completed.") { + ParentActivityId = p.Parent != null ? p.Parent.Id : 0, + PercentComplete = 100, + RecordType = ProgressRecordType.Completed + }); + } else { + WriteProgress(new ProgressRecord(p.Id, p.Activity, "Completed.") { + ParentActivityId = p.Parent != null ? p.Parent.Id : 0, + PercentComplete = 100, + RecordType = ProgressRecordType.Completed + }); + } + } + } + } + + // rather than wait on the result of the async WriteVerbose, + // we'll just return the stopping state. + return IsCanceled; + } + + public virtual string GetMessageString(string messageText, string defaultText) { + return null; + } + + public string FormatMessageString(string messageText, params object[] args) { + if (string.IsNullOrWhiteSpace(messageText)) { + return string.Empty; + } + + if (messageText.IndexOf(Microsoft.PackageManagement.Internal.Constants.MSGPrefix, StringComparison.CurrentCultureIgnoreCase) == 0) { + messageText = GetMessageString(messageText.Substring(Microsoft.PackageManagement.Internal.Constants.MSGPrefix.Length), messageText) ?? messageText; + } + + return args == null || args.Length == 0 ? messageText : messageText.format(args); + } + + protected virtual void Init() { + } + + protected virtual void Dispose(bool disposing) { + if (disposing) { + if (CancellationEvent != null) { + CancellationEvent.Dispose(); + CancellationEvent = null; + } + + // According to http://msdn.microsoft.com/en-us/library/windows/desktop/ms714463(v=vs.85).aspx + // Powershell will dispose the cmdlet if it implements IDisposable. + + if (_messages != null) { + _messages.Dispose(); + _messages = null; + } + } + } + + #region Dynamic Paramters + + public virtual RuntimeDefinedParameterDictionary DynamicParameterDictionary { + get { + return _dynamicParameters ?? (_dynamicParameters = new RuntimeDefinedParameterDictionary()); + } + } + + public object GetDynamicParameters() { + if (DynamicParameterDictionary.IsNullOrEmpty()) { + // this is because in cmdletwith provider we check for the state to determine the error we should throw + // so this state should be at least generate parameters and not unknown. + // otherwise, command like: Install-Package -Provider nuget -Destination C:\test -Name jquery -Source wrongsource + // will give us wrong error that the dynamic parameter destination does not exist even though the real error is + // that the source is wrong. + CmdletState = AsyncCmdletState.GenerateParameters; + if (IsOverridden(Constants.Methods.GenerateDynamicParametersMethod)) { + AsyncRun(GenerateDynamicParameters); + } + } + + return DynamicParameterDictionary; + } + + protected T GetDynamicParameterValue(string parameterName) { + if (DynamicParameterDictionary.ContainsKey(parameterName)) { + var p = DynamicParameterDictionary[parameterName]; + if (p.IsSet) { + if (typeof (T) == typeof (string[])) { + if (p.Value == null) { + return default(T); + } + + if (p.Value is string[]) { + return (T)p.Value; + } + + if (p.Value is string) { + return (T)(object)new string[1] {p.Value.ToString()}; + } + + if (p.Value is IEnumerable) { + return (T)(object)((IEnumerable)p.Value).Cast().Select(each => each.ToString()).ToArray(); + } + + // weird, can't get a collection from whatever is here. + return (T)(object)new string[1] {p.Value.ToString()}; + } + + if (typeof (T) == typeof (string)) { + if (p.Value == null) { + return default(T); + } + return (T)(object)p.Value.ToString(); + } + + return (T)p.Value; + } + } + return default(T); + } + + public virtual bool GenerateDynamicParameters() { + return true; + } + + public virtual bool ConsumeDynamicParameters() { + return true; + } + + #endregion + + #region cmdlet processing + + private void ProcessHeldMessages() { + if (_heldMessages != null && _heldMessages.Count > 0) { + foreach (var msg in _heldMessages.GetConsumingEnumerable().Where(msg => msg != null)) { + InvokeMessage(msg); + } + _heldMessages.Dispose(); + } + _heldMessages = null; + } + + private void AsyncRun(Func asyncAction) { + try { + using (_messages = new BlockingCollection>()) { + // spawn the activity off in another thread. + Task.Factory.StartNew(() => { + try { + if (!IsInitialized) { + Init(); + } + + return asyncAction(); + } catch (Exception e) { + Error(Constants.Errors.UnhandledException, e.Message, e.GetType().Name, e.StackTrace); + } finally { + // when the task is done, mark the msg queue as complete + if (_messages != null) { + _messages.CompleteAdding(); + } + } + return false; + }, TaskCreationOptions.LongRunning); + + + // process the queue of messages back in the main thread so that they + // can properly access the non-thread-safe-things in cmdlet + foreach (var message in _messages.GetBlockingEnumerable(CancellationEvent.Token)) { + InvokeMessage(message); + } + } + + if (_pipelineStopped != null) { + throw _pipelineStopped; + } + } finally { + _messages = null; + _pipelineStopped = null; + } + + } + + private bool IsOverridden(string functionName) { + return GetType().GetMethod(functionName).DeclaringType != typeof (AsyncCmdlet); + } + + protected override sealed void BeginProcessing() { +#if DEBUG + Log("BeginProcessing","[{0}/{1}] «{2}»".format( MyInvocation.PipelineLength, MyInvocation.PipelinePosition, MyInvocation.Line)); +#endif + try { + CmdletState = AsyncCmdletState.BeginProcess; + + ProcessHeldMessages(); + + if (IsCanceled) { + return; + } + + // let's not even bother doing all this if they didn't even + // override the method. + if (IsOverridden(Constants.Methods.BeginProcessingAsyncMethod)) { + // just before we kick stuff off, let's make sure we consume the dynamicaparmeters + if (!_consumedDynamicParameters) { + ConsumeDynamicParameters(); + _consumedDynamicParameters = true; + } + // just use our async/message pump to handle this activity + AsyncRun(BeginProcessingAsync); + } + } finally { + if (CmdletState == AsyncCmdletState.BeginProcess) { + // If the state was changed elsewhere, don't assume that we're in the next state + CmdletState = AsyncCmdletState.BeginProcessCompleted; + } + } + } + + protected override sealed void ProcessRecord() { +#if DEBUG + Log("ProcessRecord", "[{0}/{1}] «{2}»".format( MyInvocation.PipelineLength, MyInvocation.PipelinePosition, MyInvocation.Line)); +#endif + + try { + CmdletState = AsyncCmdletState.ProcessRecord; + ProcessHeldMessages(); + + // let's not even bother doing all this if they didn't even + // override the method. + if (IsOverridden(Constants.Methods.ProcessRecordAsyncMethod)) { + // just before we kick stuff off, let's make sure we consume the dynamicaparmeters + if (!_consumedDynamicParameters) { + ConsumeDynamicParameters(); + _consumedDynamicParameters = true; + } + + // just use our async/message pump to handle this activity + AsyncRun(ProcessRecordAsync); + } + } finally { + if (CmdletState == AsyncCmdletState.ProcessRecord) { + // If the state was changed elsewhere, don't assume that we're in the next state + CmdletState = AsyncCmdletState.ProcessRecordCompleted; + } + } + } + + protected override sealed void EndProcessing() { +#if DEBUG + Log("EndProcessing","[{0}/{1}] «{2}»".format( MyInvocation.PipelineLength, MyInvocation.PipelinePosition, MyInvocation.Line)); +#endif + + try { + CmdletState = AsyncCmdletState.EndProcess; + ProcessHeldMessages(); + + // let's not even bother doing all this if they didn't even + // override the method. + if (IsOverridden(Constants.Methods.EndProcessingAsyncMethod)) { + // just before we kick stuff off, let's make sure we consume the dynamicaparmeters + if (!_consumedDynamicParameters) { + ConsumeDynamicParameters(); + _consumedDynamicParameters = true; + } + + // just use our async/message pump to handle this activity + AsyncRun(EndProcessingAsync); + } + + // make sure that we mark progress complete. + if (_progressTrackers.Any()) { + AllProgressComplete(); + } + } finally { + if (CmdletState == AsyncCmdletState.EndProcess) { + // If the state was changed elsewhere, don't assume that we're in the next state + CmdletState = AsyncCmdletState.EndProcessCompleted; + } + } + } + + protected override sealed void StopProcessing() { +#if DEBUG + Log("StopProcessing","[{0}/{1}] «{2}»".format( MyInvocation.PipelineLength, MyInvocation.PipelinePosition, MyInvocation.Line)); +#endif + + try { + CmdletState = AsyncCmdletState.StopProcess; + Cancel(); + // let's not even bother doing all this if they didn't even + // override the method. + if (IsOverridden(Constants.Methods.StopProcessingAsyncMethod)) { + // just use our async/message pump to handle this activity + AsyncRun(StopProcessingAsync); + } + if (_progressTrackers.Any()) { + AllProgressComplete(); + } + } finally { + if (CmdletState == AsyncCmdletState.StopProcess) { + CmdletState = AsyncCmdletState.StopProcessCompleted; + } + } + } + + public void Cancel() { + // notify anyone listening that we're stopping this call. + if (CancellationEvent != null) { + CancellationEvent.Cancel(); + } + } + + #endregion + + #region progress + + public new Task WriteProgress(ProgressRecord progressRecord) { + return QueueMessage(() => { + if (!IsCanceled) { + base.WriteProgress(progressRecord); + } + }); + } + + public Task AllProgressComplete() { + lock (_progressTrackers) { + while (_progressTrackers.Any()) { + CompleteProgress(_progressTrackers.FirstOrDefault().Id, true); + } + } + return IsCanceled.AsResultTask(); + } + + #endregion + + #region Async/Messaging + + protected bool IsProcessing { + get { + return CmdletState == AsyncCmdletState.BeginProcess || CmdletState == AsyncCmdletState.ProcessRecord || CmdletState == AsyncCmdletState.EndProcess; + } + } + + protected bool IsBeforeProcessing { + get { + return CmdletState < AsyncCmdletState.BeginProcess; + } + } + + protected bool IsAfterProcessing { + get { + return CmdletState > AsyncCmdletState.EndProcess; + } + } + + private void InvokeMessage(TaskCompletionSource message) { + var func = message.Task.AsyncState as Func; + if (func != null) { + try { + message.SetResult(func()); + } catch (PipelineStoppedException pipelineStoppedException) { + _pipelineStopped = pipelineStoppedException; + Cancel(); + message.SetException(pipelineStoppedException); + } catch (Exception e) { + message.SetException(e); + } + } else { + // this should have been a Func. + // cancel it. + message.SetCanceled(); + } + } + + private void QueueHeldMessage(TaskCompletionSource message) { + if (message != null) { + _heldMessages = _heldMessages ?? new BlockingCollection>(); + _heldMessages.Add(message); + } + } + + protected void QueueHeldMessage(Func action) { + if (IsProcessing) { + // run it now... + action(); + } else { + QueueHeldMessage(new TaskCompletionSource(action)); + } + } + + private Task QueueMessage(TaskCompletionSource message) { + // if we're not actually into the processing step yet, we're gonna store this message + // until later. It is possible that it never gets played... + + if (IsBeforeProcessing || IsAfterProcessing) { + QueueHeldMessage(message); + return message.Task; + } + + if (_messages == null || _messages.IsCompleted) { + // message queue isn't active. Just run the message now. + InvokeMessage(message); + } else { + if (!_messages.IsCompleted) { + _messages.Add(message); + } + } + return message.Task; + } + + private Task QueueMessage(Func action) { + return QueueMessage(new TaskCompletionSource(action)); + } + + private Task QueueMessage(Action action) { + return QueueMessage(() => { + action(); + return true; + }); + } + + public Task ExecuteOnMainThread(Func onMainThreadDelegate) { + var message = new TaskCompletionSource(onMainThreadDelegate); + + if (_messages == null || _messages.IsCompleted) { + // message queue isn't active. Just run the message now. + InvokeMessage(message); + } else { + if (!_messages.IsCompleted) { + _messages.Add(message); + } + } + return message.Task; + } + + #endregion + + #region PowerShell response streams + + public new Task WriteObject(object obj) { + return QueueMessage(() => { + if (!IsCanceled) { + try { + base.WriteObject(obj); + } catch (PipelineStoppedException pipelineStoppedException) { + // this can throw if the pipeline is stopped + // but that's ok, because it just means + // that we're done. + Cancel(); + _pipelineStopped = pipelineStoppedException; + } catch { + // any other means that we're done anyway too. + } + } + }); + } + + public new Task WriteObject(object sendToPipeline, bool enumerateCollection) { + return QueueMessage(() => { + if (!IsCanceled) { + try { + base.WriteObject(sendToPipeline, enumerateCollection); + } catch (PipelineStoppedException pipelineStoppedException) { + // this can throw if the pipeline is stopped + // but that's ok, because it just means + // that we're done. + Cancel(); + _pipelineStopped = pipelineStoppedException; + } catch { + // any other means that we're done anyway too. + } + } + }); + } + + public new Task WriteWarning(string text) { + if (!IsInvocation) { + return false.AsResultTask(); + } + // ensure the same warning doesn't get played repeatedly. + if (_warnings.Contains(text)) { + return true.AsResultTask(); + } + _warnings.Add(text); + return QueueMessage(() => base.WriteWarning(text)); + } + + public new Task WriteDebug(string text) { + return QueueMessage(() => { + try { + base.WriteDebug(text); + } catch (PipelineStoppedException pipelineStoppedException) { + // this can throw if the pipeline is stopped + // but that's ok, because it just means + // that we're done. + Cancel(); + _pipelineStopped = pipelineStoppedException; + } catch { + // any other means that we're done anyway too. + } + }); + } + + public new Task WriteError(ErrorRecord errorRecord) { + if (!IsInvocation) { + return false.AsResultTask(); + } + return QueueMessage(() => { + try { + // The cancel could be caused by Error.But this error can get lost because of the If(!IsCanceled) check. + // So call WriteError directly. + base.WriteError(errorRecord); + // if we're stopping, skip this call anyway. + //if (!IsCanceled) { + // base.WriteError(errorRecord); + //} + } catch (PipelineStoppedException pipelineStoppedException) { + // this can throw if the pipeline is stopped + // but that's ok, because it just means + // that we're done. + Cancel(); + _pipelineStopped = pipelineStoppedException; + } catch { + // any other means that we're done anyway too. + } + }); + } + + public new Task WriteVerbose(string text) { + if (!IsInvocation) { + return false.AsResultTask(); + } + return QueueMessage(() => { + try { + // if we're stopping, skip this call anyway. + base.WriteVerbose(text); + } catch (PipelineStoppedException pipelineStoppedException) { + // this can throw if the pipeline is stopped + // but that's ok, because it just means + // that we're done. + Cancel(); + _pipelineStopped = pipelineStoppedException; + } catch { + // any other means that we're done anyway too. + } + }); + } + + #endregion + + #region CmdLet Interactivity + + internal class ShouldContinueResult + { + internal bool result = false; + internal bool yesToAll = false; + internal bool noToAll = false; + } + + public new Task ShouldContinue(string query, string caption) { + if (IsCanceled || !IsInvocation) { + return false.AsResultTask(); + } + return ExecuteOnMainThread(() => base.ShouldContinue(query, caption)); + // it is apparently OK to have this called during dynamic parameter generation + } + + internal Task ShouldContinue(string query, string caption, bool hasSecurityimpact) + { + var shouldContinueResult = new ShouldContinueResult + { + yesToAll = false, + noToAll = false, + result = false + }; + + if (IsCanceled || !IsInvocation ) + { + return shouldContinueResult.AsResultTask(); + } + + return ExecuteOnMainThread(() => + { + MethodInfo method = null; + try { + method = base.GetType().GetMethod("ShouldContinue", new Type[] { typeof(string), typeof(string), typeof(bool), typeof(bool).MakeByRefType(), typeof(bool).MakeByRefType()}); + } catch { + } + if(method != null) { + Object[] arguments = { query, caption, hasSecurityimpact, false, false }; + try { + shouldContinueResult.result = (bool)method.Invoke(this, arguments); + shouldContinueResult.yesToAll = (bool)arguments[3]; + shouldContinueResult.noToAll = (bool)arguments[4]; + } catch { + } + } else { + shouldContinueResult.result = base.ShouldContinue(query, caption, ref shouldContinueResult.yesToAll, ref shouldContinueResult.noToAll); + } + return shouldContinueResult.result; + }).ContinueWith((a) => shouldContinueResult); + } + + public new Task ShouldProcess(string target) { + if (IsCanceled || !IsInvocation) { + return false.AsResultTask(); + } + + return QueueMessage(() => base.ShouldProcess(target)); + } + + public new Task ShouldProcess(string target, string action) { + if (IsCanceled || !IsInvocation) { + return false.AsResultTask(); + } + + return QueueMessage(() => base.ShouldProcess(target, action)); + } + + public new Task ShouldProcess(string verboseDescription, string verboseWarning, string caption) { + if (IsCanceled || !IsInvocation) { + return false.AsResultTask(); + } + + return QueueMessage(() => base.ShouldProcess(verboseDescription, verboseWarning, caption)); + } + + public new Task ShouldProcess(string verboseDescription, string verboseWarning, string caption, out ShouldProcessReason shouldProcessReason) { + if (IsCanceled || !IsInvocation) { + shouldProcessReason = ShouldProcessReason.None; + return false.AsResultTask(); + } + + // todo: Uh, this is gonna be tricky!? + shouldProcessReason = ShouldProcessReason.None; + return QueueMessage(() => base.ShouldProcess(verboseDescription, verboseWarning, caption)); + } + + #endregion + + #region Direct Property Access + + protected object TryGetProperty(object instance, string fieldName) { + // any access of a null object returns null. + if (instance == null || string.IsNullOrWhiteSpace(fieldName)) { + return null; + } + + var propertyInfo = instance.GetType().GetProperty(fieldName, BindingFlags); + + if (propertyInfo != null) { + try { + return propertyInfo.GetValue(instance, null); + } catch { + } + } + + // maybe it's a field + var fieldInfo = instance.GetType().GetField(fieldName, BindingFlags); + + if (fieldInfo != null) { + try { + return fieldInfo.GetValue(instance); + } catch { + } + } + + // no match, return null. + return null; + } + + protected bool TrySetProperty(object instance, string fieldName, object value) { + // any access of a null object returns null. + if (instance == null || string.IsNullOrWhiteSpace(fieldName)) { + return false; + } + + var propertyInfo = instance.GetType().GetProperty(fieldName, BindingFlags); + + if (propertyInfo != null) { + try { + propertyInfo.SetValue(instance, value, null); + return true; + } catch { + } + } + + // maybe it's a field + var fieldInfo = instance.GetType().GetField(fieldName, BindingFlags); + + if (fieldInfo != null) { + try { + fieldInfo.SetValue(instance, value); + return true; + } catch { + } + } + + return false; + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/CmdletBase.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/CmdletBase.cs new file mode 100644 index 000000000..85d9ddbc1 --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/CmdletBase.cs @@ -0,0 +1,542 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets { + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.IO; + using System.Linq; + using System.Management.Automation; + using System.Security; + using System.Threading; + using Microsoft.PackageManagement; + using Microsoft.PackageManagement.Implementation; + using Microsoft.PackageManagement.Internal; + using Microsoft.PackageManagement.Internal.Api; + using Microsoft.PackageManagement.Internal.Implementation; + using Microsoft.PackageManagement.Internal.Utility.Async; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Resources; + using Utility; + using Constants = PackageManagement.Constants; + using System.Diagnostics.CodeAnalysis; + using System.Globalization; + using Microsoft.PackageManagement.Packaging; + using Telemetry.Internal; + using System.Management.Automation.Runspaces; + using System.Net; + using Microsoft.PackageManagement.Internal.Utility.Plugin; + + + public delegate string GetMessageString(string messageId, string defaultText); + + public abstract class CmdletBase : AsyncCmdlet, IHostApi { + private static int _globalCallCount = 1; + private static readonly object _lockObject = new object(); + private bool _messageResolverNotResponding = false; + private readonly int _callCount; + private readonly Hashtable _dynamicOptions = new Hashtable(); + private string _bootstrapNuGet = "false"; + + [Parameter] + public SwitchParameter Force; + + [Parameter] + public SwitchParameter ForceBootstrap; + + private GetMessageString _messageResolver; + + protected CmdletBase() { + _callCount = _globalCallCount++; + ShouldLogError = true; + } + + protected bool IsRooted(string filePath) + { + return (Path.IsPathRooted(filePath) || + filePath.StartsWith(@".\", StringComparison.Ordinal) || + filePath.StartsWith(@"./", StringComparison.Ordinal) || + filePath.StartsWith(@"..\", StringComparison.Ordinal) || + filePath.StartsWith(@"../", StringComparison.Ordinal) || + filePath.StartsWith(@"~/", StringComparison.Ordinal) || + filePath.StartsWith(@"~\", StringComparison.Ordinal)); + } + + protected void ValidateVersion(string version) { + if (string.IsNullOrWhiteSpace(version)) { + return; + } + try { + Version.Parse(version); + } catch (Exception ex) { + Error(Constants.Errors.InvalidVersion, version, ex.Message); + } + } + + protected bool ShouldLogError {get; set;} + + protected bool ShouldSelectAllProviders { get; set; } + + protected bool WaitForActivity(IEnumerable> enumerables) { + var handles = enumerables.Select(each => each.Ready).ConcatSingleItem(CancellationEvent.Token.WaitHandle).ToArray(); + if (handles.Length > 1) { + if (handles.Length <= 64) + { + // less than or equal to 64 handles so we can use waithandle + WaitHandle.WaitAny(handles); + } + else + { + // throw error saying we don't support + Error(Constants.Errors.TooManyPackages, handles.Length); + + //// divide handles in chunks of 64 each + //int numberOfTasks = (int)Math.Ceiling(handles.Length / 64.0); + //var cts = new CancellationTokenSource(); + //Task[] tasks = new Task[numberOfTasks]; + //for (int i = 0; i < numberOfTasks; i += 1) + //{ + // // now each task will use the waithandle.waitany since only 64 handles will be assigned to each task + // // so we won't get exception + // tasks[i] = Task.Factory.StartNew(() => + // { + // if ((handles.Length - i * 64) > 0) + // { + // // either 64 items or whatever left + // int waitHandleArrayLength = Math.Min(64, handles.Length - i * 64); + // WaitHandle[] waithandles = new WaitHandle[waitHandleArrayLength]; + // Array.Copy(handles, i * 64, waithandles, 0, waitHandleArrayLength); + // WaitHandle.WaitAny(waithandles); + // } + // }, cts.Token); + //} + + //// just need a task to finish + //Task.WaitAny(tasks); + //// cancel the rest + //cts.Cancel(); + } + return !IsCanceled; + } + // we are finished when there is only handle to wait on (the cancellation token) + return false; + } + + protected abstract IEnumerable ParameterSets {get;} + + protected bool IsPackageBySearch { + get { + return ParameterSetName == Constants.ParameterSets.PackageBySearchSet; + } + } + + protected bool IsPackageByObject { + get { + return ParameterSetName == Constants.ParameterSets.PackageByInputObjectSet; + } + } + + protected bool IsSourceByObject { + get { + return ParameterSetName == Constants.ParameterSets.SourceByInputObjectSet; + } + } + + protected internal IPackageManagementService PackageManagementService { + get { + lock (_lockObject) { + if (!IsCanceled && !IsInitialized) { + try { + IsInitialized = PackageManager.Instance.Initialize(this); + } catch (Exception e) { + e.Dump(); + } + } + } + return PackageManager.Instance; + } + } + + protected int TimeOut { + get { + if (IsInvocation) { + var t = GetDynamicParameterValue("Timeout"); + if (t > 0) { + return t; + } + } + + return Constants.DefaultTimeout; // in a non-invokcation, always default to one hour + } + } + + protected int Responsiveness { + get { + if (IsInvocation) { + var t = GetDynamicParameterValue("Responsiveness"); + if (t > 0) { + return t; + } + } + return Constants.DefaultResponsiveness; // in a non-invokcation, always default to one hour + } + } + + public GetMessageString MessageResolver { + get { + return _messageResolver ?? (_messageResolver = GetDynamicParameterValue("MessageResolver")); + } + } + + #region Implementing IHostApi + + public Hashtable DynamicOptions { + get { + return _dynamicOptions; + } + } + + public virtual IEnumerable Sources { + get { + return null; + } + set { + } + } + + public virtual IEnumerable OptionKeys { + get { + return DynamicParameterDictionary.Values.OfType().Where(each => each.IsSet).Select(each => each.Name).Concat(MyInvocation.BoundParameters.Keys); + } + } + + protected bool GenerateCommonDynamicParameters() { + if (IsInvocation) { + // only generate these parameters if there is an actual call happening. + // this prevents get-help from showing them. + DynamicParameterDictionary.Add("Timeout", new RuntimeDefinedParameter("Timeout", typeof (int), new Collection { + new ParameterAttribute() + })); + + DynamicParameterDictionary.Add("Responsiveness", new RuntimeDefinedParameter("Responsiveness", typeof (int), new Collection { + new ParameterAttribute() + })); + + DynamicParameterDictionary.Add("MessageResolver", new RuntimeDefinedParameter("MessageResolver", typeof (GetMessageString), new Collection { + new ParameterAttribute() + })); + } + return true; + } + + public override string GetMessageString(string messageText, string defaultText) { + messageText = DropMsgPrefix(messageText); + + if (string.IsNullOrWhiteSpace(defaultText) || defaultText.IndexOf("MSG:", StringComparison.OrdinalIgnoreCase) == 0) { + defaultText = Messages.ResourceManager.GetString(messageText); + } + + string result = null; + //when a user does find-module xjea, psreadline | install-module, the install-module starts executing before find-module completes. This + //is expected. However when the install-module starts, the MessageResolver delegate stops responding. This is a PowerShell thing. + //Once the delegate is blocked, it's blocked for the subsequence log message calls for the current cmdlet, find-module. Note that the + //next cmdlet, install-module has no impact on the delegate blocking issue, meaning all messages are expected to be logged for install-module. + //_messageResolverNotResponding is for avoiding continuesly waiting for unresponsive MessageResolver delegate. + if (MessageResolver != null && !_messageResolverNotResponding) { + // if the consumer has specified a MessageResolver delegate, we need to call it on the main thread + // because powershell won't let us use the default runspace from another thread. + + // if we are anywhere but the end of the pipeline, the delegate here would block on stuff later in the pipe + // and since we're blocking *that* based on the the resolution of this, we're better off just skipping + // the message resoluion for things earlier in the pipeline. + _messageResolverNotResponding = !ExecuteOnMainThread(() =>{ + result = MessageResolver(messageText, defaultText); + if (string.IsNullOrWhiteSpace(result)) { + result = null; + } + return true; + }).Wait(5000); // you don't get a lot of time to get back to me on this... + } + + return result ?? Messages.ResourceManager.GetString(messageText) ?? defaultText; + } + + public virtual bool AskPermission(string permission) { +#if DEBUG + Message(Constants.Messages.NotImplemented, permission); +#endif + return true; + } + + public virtual IEnumerable GetOptionValues(string key) { + // try to see whether the key is a dynamic parameter first. if we cannot find it then go to bound parameters. + var dynamicValues = DynamicParameterDictionary.Values.OfType().Where(each => each.IsSet && each.Name == key).SelectMany(each => each.GetValues(this)); + if (dynamicValues.Any()) + { + return dynamicValues; + } + + if (MyInvocation.BoundParameters.ContainsKey(key)) { + var value = MyInvocation.BoundParameters[key]; + if (value is string || value is int) { + return new[] { + MyInvocation.BoundParameters[key].ToString() + }; + } + if (value is SwitchParameter) { + return new[] { + ((SwitchParameter)MyInvocation.BoundParameters[key]).IsPresent.ToString() + }; + } + if (value is string[]) { + return ((string[])value); + } + return new[] { + MyInvocation.BoundParameters[key].ToString() + }; + } + + return Enumerable.Empty(); + } + + public virtual IWebProxy WebProxy + { + get + { + return null; + } + } + + public virtual string CredentialUsername { + get { + return null; + } + } + + public virtual SecureString CredentialPassword { + get { + return null; + } + } + + public virtual bool ShouldContinueWithUntrustedPackageSource(string package, string packageSource) { +#if DEBUG + Message(Constants.Messages.NotImplemented, packageSource); +#endif + return true; + } + + [SuppressMessage("Microsoft.Design", "CA1045:DoNotPassTypesByReference", Justification = "It's a PowerShell behavior.")] + bool IHostApi.ShouldContinue(string query, string caption, ref bool yesToAll, ref bool noToAll) + { + var shouldContinueResult = base.ShouldContinue(query, caption, true).Result; + if (shouldContinueResult != null) + { + yesToAll = shouldContinueResult.yesToAll; + noToAll = shouldContinueResult.noToAll; + return shouldContinueResult.result; + } + + return false; + } + + bool IHostApi.ShouldContinue(string query, string caption) + { + return base.ShouldContinue(query, caption).Result; + } + + public virtual bool ShouldBootstrapProvider(string requestor, string providerName, string providerVersion, string providerType, string location, string destination) { + try { + if (Force || ForceBootstrap) { + return true; + } + + return ShouldContinue(FormatMessageString(Constants.Messages.QueryBootstrap, providerName), + FormatMessageString(Constants.Messages.BootstrapProvider, + !string.IsNullOrWhiteSpace(requestor) ? + FormatMessageString(Constants.Messages.BootstrapProviderProviderRequested, requestor, providerName, providerVersion) : + FormatMessageString(Constants.Messages.BootstrapProviderUserRequested, providerName, providerVersion), + !string.IsNullOrWhiteSpace(providerType) && providerType.Equals(Constants.AssemblyProviderType) ? + FormatMessageString(Constants.Messages.BootstrapManualAssembly, providerName, location, destination) : + FormatMessageString(Constants.Messages.BootstrapManualInstall, providerName, location))).Result; + } catch (Exception e) { + e.Dump(); + } + return false; + } + + public virtual bool IsInteractive { + get { + return IsInvocation; + } + } + + public virtual int CallCount { + get { + return _callCount; + } + } + + #endregion + + protected override void Init() { + if (!IsInitialized) { + // get the service ( forces initialization ) + var x = PackageManagementService; + } + } + + protected virtual string BootstrapNuGet + { + get + { + return _bootstrapNuGet; + } + set + { + _bootstrapNuGet = value; + } + } + + protected virtual IHostApi PackageManagementHost + { + get + { + IHostApi parent = this; + return new object[] { + new { + GetOptionValues = new Func>(key => { + if (key.EqualsIgnoreCase(Microsoft.PackageManagement.Internal.Constants.BootstrapNuGet)) { + return BootstrapNuGet.SingleItemAsEnumerable(); + } + + return this.GetOptionValues(key); + }), + }, + parent, + }.As(); + } + } + + protected IEnumerable SelectProviders(string[] names) { + if (names.IsNullOrEmpty()) { + return ShouldSelectAllProviders ? PackageManagementService.SelectProviders(null, PackageManagementHost.SuppressErrorsAndWarnings(IsProcessing)) + : PackageManagementService.SelectProviders(null, PackageManagementHost.SuppressErrorsAndWarnings(IsProcessing)).Where(each => !each.Features.ContainsKey(Microsoft.PackageManagement.Internal.Constants.Features.AutomationOnly)); + + } + // you can manually ask for any provider by name, if it is for automation only. + if (IsInvocation) { + + return names.SelectMany(each => PackageManagementService.SelectProviders(each, this)); + } + return names.SelectMany(each => PackageManagementService.SelectProviders(each, PackageManagementHost.SuppressErrorsAndWarnings(IsProcessing))); + } + + protected IEnumerable SelectProviders(string name) { + // you can manually ask for any provider by name, if it is for automation only. + if (IsInvocation) { + var result = PackageManagementService.SelectProviders(name, PackageManagementHost).ToArray(); + if ((result.Length == 0) && ShouldLogError) { + Error(Constants.Errors.UnknownProvider, name); + } + return result; + } + + var r = PackageManagementService.SelectProviders(name, PackageManagementHost.SuppressErrorsAndWarnings(IsProcessing)).ToArray(); + if ((r.Length == 0) && ShouldLogError){ + Error(Constants.Errors.UnknownProvider, name); + } + return r; + } + + public override bool ConsumeDynamicParameters() { + // pull data from dynamic parameters and place them into the DynamicOptions collection. + foreach (var rdp in DynamicParameterDictionary.Keys.Select(d => DynamicParameterDictionary[d]).Where(rdp => rdp.IsSet)) { + if (rdp.ParameterType == typeof (SwitchParameter)) { + _dynamicOptions[rdp.Name] = true; + } else { + _dynamicOptions[rdp.Name] = rdp.Value; + } + } + return true; + } + + #region Event and telemetry stuff + //Calling PowerShell Telemetry APIs + protected void TraceMessage(string message, SoftwareIdentity swidObject) { + TelemetryAPI.TraceMessage(message, + new { + PackageName = swidObject.Name, + PackageVersion = swidObject.Version, + PackageProviderName = swidObject.ProviderName, + Repository = swidObject.Source, + ExuectionStatus = swidObject.Status, + ExecutionTime = DateTime.Today + }); + } + + protected enum EventTask { + None = 0, + Install = 1, + Uninstall = 2, + Download = 3 + } + + + //We use Microsoft-Windows-PowerShell event source to log PackageManagement events. + //4101... is the eventid of Microsoft-Windows-PowerShell event provider + //You can find the events in Microsoft-Windows-PowerShell event log + protected enum EventId + { + Install = 4101, + Uninstall = 4102, + Save = 4103 + } + + protected void LogEvent(EventTask task, EventId id, string context, string name, string version, string providerName, string source, string status, string destinationPath) + { + var iis = InitialSessionState.CreateDefault2(); + + using (Runspace rs = RunspaceFactory.CreateRunspace(iis)) + { + using (PowerShell powershell = PowerShell.Create()) + { + try + { + rs.Open(); + powershell.Runspace = rs; + powershell.AddCommand(Constants.NewWinEvent); + powershell.AddParameter("ProviderName", Constants.PowerShellProviderName); + powershell.AddParameter("Id", id); + + powershell.AddParameter("Payload", new object[] { + task.ToString(), + string.Format(CultureInfo.InvariantCulture, "Package={0}, Version={1}, Provider={2}, Source={3}, Status={4}, DestinationPath={5}", name, version, providerName, source, status, destinationPath), + context}); + + powershell.Invoke(); + } + catch (Exception ex) + { + Verbose(ex.Message); + } + } + } + } + + #endregion + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/CmdletWithProvider.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/CmdletWithProvider.cs new file mode 100644 index 000000000..cf04da20e --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/CmdletWithProvider.cs @@ -0,0 +1,631 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets { + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Management.Automation; + using Microsoft.PackageManagement.Implementation; + using Microsoft.PackageManagement.Internal.Implementation; + using Microsoft.PackageManagement.Internal.Packaging; + using Microsoft.PackageManagement.Internal.Utility.Collections; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Utility; + using Microsoft.PackageManagement.Packaging; + + public abstract class CmdletWithProvider : CmdletBase { + private readonly OptionCategory[] _optionCategories; + protected CmdletWithProvider(OptionCategory[] categories) { + _optionCategories = categories; + } + + private string[] _providerName; + private bool _initializedTypeName; + private string _softwareIdentityTypeName = "Microsoft.PackageManagement.Packaging.SoftwareIdentity"; + private bool _isDisplayCulture; + private bool _initializedCulture; + private bool _isUserSpecifyOneProviderName; + private bool _hasTypeNameChanged; + private bool _useDefaultSourceFormat = true; + private bool _initializedSource; + private IEnumerable _resolvedUserSpecifiedSource; + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "Used in a powershell parameter.")] + public string[] ProviderName { + get { + if (!_providerName.IsNullOrEmpty()) { + //meaning a user specifies -ProviderName, we will use it + return _providerName; + } + //need to call it so that cases like get-packagesource | find-package -name Jquery will work + return GetDynamicParameterValue("ProviderName"); + } + set { + _providerName = value; + } + } + + protected bool IsFailingEarly; + protected Dictionary UserSpecifiedSourcesList = new Dictionary(StringComparer.OrdinalIgnoreCase); + + protected virtual IEnumerable SelectedProviders { + get { + + var didUserSpecifyProviders = !ProviderName.IsNullOrEmpty(); + var registeredSources = Enumerable.Empty(); + + + // filter on provider names - if they specify a provider name, narrow to only those provider names. + // if this is an actual invocation, this will attempt to bootstrap a provider that the user specified + // (which will require a prompt or -force or -forcebootstrap ) + var providers = SelectProviders(ProviderName).ReEnumerable(); + + if (!providers.Any()) { + // the user gave us provider names that we're not able to resolve. + + if (IsInvocation) { + // and we're in an actual cmdlet invocaton. + QueueHeldMessage(() => Error(Constants.Errors.UnknownProviders, ProviderName.JoinWithComma())); + IsFailingEarly = true; + } + // return the empty collection, for all the good it's doing. + return providers; + } + + // fyi, re-enumerable insulates us against null sets. + var userSpecifiedSources = Sources.ReEnumerable().ToArray(); + var didUserSpecifySources = userSpecifiedSources.Any(); + + // filter out providers that don't have the sources that have been specified (only if we have specified a source!) + if (didUserSpecifySources) { + // sources must actually match a name or location. Keeps providers from being a bit dishonest + + var potentialSources = providers.SelectMany(each => each.ResolvePackageSources(this.SuppressErrorsAndWarnings(IsProcessing)) + .Where(source => userSpecifiedSources.Any( + // check whether location of the resolved source contains the userspecified source + userSpecifiedSource => source.Location.EqualsIgnoreEndSlash(userSpecifiedSource) + // or the name equals to the name provided by the user + || source.Name.EqualsIgnoreCase(userSpecifiedSource)))).ReEnumerable(); + + // save the resolved package source name and location + potentialSources.SerialForEach(src => UserSpecifiedSourcesList.AddOrSet(src.Name, src.Location)); + + // prefer registered sources + registeredSources = potentialSources.Where(source => source.IsRegistered).ReEnumerable(); + + _resolvedUserSpecifiedSource = registeredSources.Any() ? registeredSources : potentialSources; + var filteredproviders = registeredSources.Any() ? registeredSources.Select(source => source.Provider).Distinct().ReEnumerable() : potentialSources.Select(source => source.Provider).Distinct().ReEnumerable(); + + if (!filteredproviders.Any()) { + // we've filtered out everthing! + + if (!didUserSpecifyProviders) { + // if cmdlet is generating parameter, we have to log error that source is wrong + if (IsInvocation && CmdletState >= AsyncCmdletState.GenerateParameters ) { + + // user didn't actually specify provider(s), the sources can't be tied to any particular provider + QueueHeldMessage(() => Error(Constants.Errors.SourceNotFound, userSpecifiedSources.JoinWithComma())); + IsFailingEarly = true; + } + // return the empty set. + return filteredproviders; + } + + // they gave us both provider name(s) and source(s) + // and the source(s) aren't found in the providers they listed + // we should log the correct error about the source. if not we will get wrong error about dynamic parameters. + if (IsInvocation && CmdletState >= AsyncCmdletState.GenerateParameters ) { + if (providers.Count() < 2) { + QueueHeldMessage(() => Error(Constants.Errors.SourceNotFound, userSpecifiedSources.JoinWithComma())); + IsFailingEarly = true; + } else { + var providerNames = providers.Select(each => each.Name).JoinWithComma(); + QueueHeldMessage(() => Error(Constants.Errors.NoMatchForProvidersAndSources, providerNames, userSpecifiedSources.JoinWithComma())); + IsFailingEarly = true; + } + } + + return filteredproviders; + } + + // make this the new subset. + providers = filteredproviders; + } + + + // filter on: dynamic options - if they specify any dynamic options, limit the provider set to providers with those options. + var result = FilterProvidersUsingDynamicParameters(providers, registeredSources, didUserSpecifyProviders, didUserSpecifySources).ToArray(); + + /* todo : return error messages when dynamic parameters filter everything out. Either here or in the FPUDP fn. + + if (!result.Any()) { + // they specified dynamic parameters that implicitly select providers + // that don't fit with the providers and sources that they initially asked for. + + if (didUserSpecifyProviders) { + + if (didUserSpecifySources) { + // user said provider and source and the dynamic parameters imply providers they didn't select + if (IsInvocation) { + QueueHeldMessage(() => Error(Errors.DynamicParameters, providerNames, userSpecifiedSources.JoinWithComma())); + } + // return empty set + return result; + } + + // user said provider and then the dynamic parameters imply providers they didn't select + if (IsInvocation) { + // error + } + // return empty set + return result; + + } + + if (didUserSpecifySources) { + // user gave sources which implied some providers but the dynamic parameters implied different providers + if (IsInvocation) { + // error + } + // return empty set + return result; + } + + // well, this is silly. + // if the user didn't specify a source or a provider + // but the FilterProvidersUsingDynamicParameters came back empty + // that means that they user specified parameters from two conflicting providers + // and they forced each other out! + + if (IsInvocation) { + // error + + } + + } + */ + return result; + } + } + + + private IEnumerable FilterProvidersUsingDynamicParameters(MutableEnumerable providers, IEnumerable userSpecifiedRegisteredSources, bool didUserSpecifyProviders, bool didUserSpecifySources) { + var excluded = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + var setparameters = DynamicParameterDictionary.Values.OfType().Where(each => each.IsSet).ReEnumerable(); + + var matchedProviders = (setparameters.Any() ? providers.Where(p => setparameters.All(each => each.Options.Any(opt => opt.ProviderName == p.ProviderName))) : providers).ReEnumerable(); + + foreach (var provider in matchedProviders) { + // if a 'required' parameter is not filled in, the provider should not be returned. + // we'll collect these for warnings at the end of the filter. + var missingRequiredParameters = DynamicParameterDictionary.Values.OfType().Where(each => !each.IsSet && each.IsRequiredForProvider(provider.ProviderName)).ReEnumerable(); + if (!missingRequiredParameters.Any()) { + yield return provider; + } else { + Collection missingOptions = new Collection(); + foreach (var missingRequiredParameter in missingRequiredParameters) + { + foreach (var option in missingRequiredParameter.Options) + { + // remember these so we can warn later. + missingOptions.Add(option.Name); + } + } + excluded.Add(provider.ProviderName, missingOptions); + } + } + + /* TODO: provide errors in the case where everything got filtered out. Or maybe warnings? + * + var mismatchedProviders = (setparameters.Any() ? providers.Where(each => !matchedProviders.Contains(each)).Where(p => setparameters.Any(each => each.Options.Any(opt => opt.ProviderName == p.ProviderName))) : Enumerable.Empty()).ReEnumerable(); + + if (!found) { + // we didn't find anything that matched + // they specified dynamic parameters that implicitly select providers + // that don't fit with the providers and sources that they initially asked for. + + if (didUserSpecifyProviders || didUserSpecifySources) { + + if (IsInvocation) { + QueueHeldMessage(() => Error(Errors.ExcludedProvidersDueToMissingRequiredParameter, excluded.Keys, userSpecifiedSources.JoinWithComma())); + } + + yield break; + + } + + if (didUserSpecifySources) { + // user gave sources which implied some providers but the dynamic parameters implied different providers + if (IsInvocation) { + // error + } + // return empty set + return result; + } + + // well, this is silly. + // if the user didn't specify a source or a provider + // but the FilterProvidersUsingDynamicParameters came back empty + // that means that they user specified parameters from two conflicting providers + // and they forced each other out! + + if (IsInvocation) { + // error + + } + + } + */ + + if (ProviderName != null && ProviderName.Any()) { + foreach (var providerName in ProviderName) { + if (excluded.ContainsKey(providerName)) { + Error(Constants.Errors.SpecifiedProviderMissingRequiredOption, providerName, excluded[providerName].JoinWithComma()); + } + } + } + + // these warnings only show for providers that would have otherwise be selected. + // if not for the missing requrired parameter. + foreach (var mp in excluded.OrderBy(each => each.Key)) { + string optionsValue = mp.Value.JoinWithComma(); + + if (userSpecifiedRegisteredSources.Any()) + { + var mp1 = mp; + + //Check if the provider with missing dynamic parameters has been registered with the source provided by a user + var sources = userSpecifiedRegisteredSources.Where(source => source.ProviderName != null && source.ProviderName.EqualsIgnoreCase(mp1.Key)); + + if (didUserSpecifySources && sources.Any()) + { + //Error out if the provider associated with the -source matches + Error(Constants.Errors.SpecifiedProviderMissingRequiredOption, mp.Key, optionsValue); + } + } + + Verbose(Constants.Messages.SkippedProviderMissingRequiredOption, mp.Key, optionsValue); + } + } + + + protected virtual void GenerateCmdletSpecificParameters(Dictionary unboundArguments) { + if (!IsInvocation) { + var providerNames = PackageManagementService.AllProviderNames; + var whatsOnCmdline = GetDynamicParameterValue("ProviderName"); + if (whatsOnCmdline != null) { + providerNames = providerNames.Concat(whatsOnCmdline).Distinct(); + } + + DynamicParameterDictionary.AddOrSet("ProviderName", new RuntimeDefinedParameter("ProviderName", typeof (string[]), new Collection { + new ParameterAttribute { + ValueFromPipelineByPropertyName = true + }, + new AliasAttribute("Provider"), + new ValidateSetAttribute(providerNames.ToArray()) + })); + } else { + DynamicParameterDictionary.AddOrSet("ProviderName", new RuntimeDefinedParameter("ProviderName", typeof(string[]), new Collection { + new ParameterAttribute { + ValueFromPipelineByPropertyName = true + }, + new AliasAttribute("Provider") + })); + } + } + + protected bool ActualGenerateDynamicParameters(Dictionary unboundArguments ) { + if (CachedStaticParameters == null) { + // we're in the second call, we're just looking to find out what the static parameters actually are. + // we're gonna just skip generating the dynamic parameters on this call. + return true; + } + + try { + + unboundArguments = unboundArguments ?? new Dictionary(); + + // if there are unbound arguments that are owned by a provider, we can narrow the rest of the + // arguments to just ones that are connected with that provider + var dynamicOptions = CachedDynamicOptions; + + var keys = unboundArguments.Keys.ToArray(); + if (keys.Length > 0) { + var acceptableProviders = CachedDynamicOptions.Where(option => keys.ContainsAnyOfIgnoreCase(option.Name)).Select(option => option.ProviderName).Distinct().ToArray(); + if (acceptableProviders.Length > 0) { + dynamicOptions = dynamicOptions.Where(option => acceptableProviders.Contains(option.ProviderName)).ToArray(); + } + } + // generate the common parameters for our cmdlets (timeout, messagehandler, etc) + GenerateCommonDynamicParameters(); + + // generate parameters that are specific to the cmdlet being implemented. + GenerateCmdletSpecificParameters(unboundArguments); + + var staticParameters = GetType().Get>("MyInvocation.MyCommand.Parameters"); + + foreach (var md in dynamicOptions) { + if (string.IsNullOrWhiteSpace(md.Name)) { + continue; + } + + if (DynamicParameterDictionary.ContainsKey(md.Name)) { + // todo: if the dynamic parameters from two providers aren't compatible, then what? + + // for now, we're just going to mark the existing parameter as also used by the second provider to specify it. + var crdp = DynamicParameterDictionary[md.Name] as CustomRuntimeDefinedParameter; + + if (crdp == null) { + // the package provider is trying to overwrite a parameter that is already dynamically defined by the BaseCmdlet. + // just ignore it. + continue; + } + + if (IsInvocation) { + // this is during an actual execution + crdp.Options.Add(md); + } else { + // this is for metadata sake. (get-help, etc) + crdp.IncludeInParameterSet(md, IsInvocation, ParameterSets); + } + } else { + // check if the dynamic parameter is a static parameter first. + + // this can happen if we make a common dynamic parameter into a proper static one + // and a provider didn't know that yet. + + if (staticParameters != null && staticParameters.ContainsKey(md.Name)) { + // don't add it. + continue; + } + + DynamicParameterDictionary.Add(md.Name, new CustomRuntimeDefinedParameter(md, IsInvocation, ParameterSets)); + } + } + } catch (Exception e) { + e.Dump(); + } + return true; + } + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "It's a performance thing.")] + protected PackageProvider[] CachedSelectedProviders { + get { + return GetType().GetOrAdd(() => SelectedProviders.ToArray(), "CachedSelectedProviders"); + } + } + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "It's a performance thing.")] + protected virtual DynamicOption[] CachedDynamicOptions { + get { + return GetType().GetOrAdd(() => CachedSelectedProviders.SelectMany(provider => _optionCategories.SelectMany(category => provider.GetDynamicOptions(category, this.SuppressErrorsAndWarnings(IsProcessing)))).ToArray(), "CachedDynamicOptions"); + } + } + + protected Dictionary CachedStaticParameters { + get { + return GetType().Get>("MyInvocation.MyCommand.Parameters"); + } + } + + public override bool GenerateDynamicParameters() { + var thisIsFirstObject = false; + try { + if (!IsReentrantLocked) { + // we're not locked at this point. Let's turn on the lock. + IsReentrantLocked = true; + thisIsFirstObject = true; + + try { + // do all the work that we need to during the lock + // this includes: + // one-time-per-call work + // any access to MyInvocation.MyCommand.* + // modifying parameter validation sets + // + + if (MyInvocation != null && MyInvocation.MyCommand != null && MyInvocation.MyCommand.Parameters != null) { + GetType().AddOrSet(MyInvocation.MyCommand.Parameters, "MyInvocation.MyCommand.Parameters"); + } +#if DEEP_DEBUG + else { + if (MyInvocation == null) { + Console.WriteLine("»»» Attempt to get parameters MyInvocation == NULL"); + } else { + if (MyInvocation.MyCommand == null) { + Console.WriteLine("»»» Attempt to get parameters MyCommand == NULL"); + } else { + Console.WriteLine("»»» Attempt to get parameters Parameters == NULL"); + } + } + } +#endif + + + // the second time, it will generate all the parameters, including the dynamic ones. + // (not that we currently need it, but if you do, you gotta do it here!) + // var all_parameters = MyInvocation.MyCommand.Parameters; + + // ask for the unbound arguments. + var unbound = UnboundArguments; + if (unbound.ContainsKey("ProviderName") || unbound.ContainsKey("Provider")) + { + var pName = unbound.ContainsKey("ProviderName")?unbound["ProviderName"]:unbound["Provider"] ; + if (pName != null) + { + + if (pName.GetType().IsArray) + { + ProviderName = pName as string[] ?? (((object[])pName).Select(p => p.ToString()).ToArray()); + } + else + { + ProviderName = new[] { pName.ToString() }; + } + + // a user specifies -providerName + _isUserSpecifyOneProviderName = (ProviderName.Count() == 1); + + } + } + + // we've now got a copy of the arguments that aren't bound + // and we can potentially narrow the provider selection based + // on arguments the user specified. + + if (null== CachedSelectedProviders || IsFailingEarly || IsCanceled ) { +#if DEEP_DEBUG + Console.WriteLine("»»» Cancelled before we got finished doing dynamic parameters"); +#endif + // this happens if there is a serious failure early in the cmdlet + // i.e. - if the SelectedProviders comes back empty (due to agressive filtering) + + // in this case, we just want to provide a catch-all for remaining arguments so that we can make + // output the error that we really want to (that the user specified conditions that filtered them all out) + + DynamicParameterDictionary.Add("RemainingArguments", new RuntimeDefinedParameter("RemainingArguments", typeof(object), new Collection { + new ParameterAttribute() { ValueFromRemainingArguments = true}, + })); + } + + // at this point, we're actually calling to have the dynamic parameters generated + // that are expected to be used. + return ActualGenerateDynamicParameters(unbound); + + } finally { + IsReentrantLocked = false; + } + } + + // otherwise just call the AGDP because we're in a reentrant call. + // and this might be needed if the original call had some strange need + // to know what the parameters that it's about to generate would be. + // Yeah, you heard me. + return ActualGenerateDynamicParameters(null); + + } finally + { + if (thisIsFirstObject) { + // clean up our once-per-call data. + GetType().Remove( "CachedSelectedProviders"); + GetType().Remove>("MyInvocation.MyCommand.Parameters"); + GetType().Remove("CachedDynamicOptions"); + } + } + // return true; + } + + // true if a user specifies -displayCulture and provider has the "DisplayCulture" dynamic option + protected bool IsDisplayCulture + { + get + { + if (!_initializedCulture) + { + var displayCulture = GetDynamicParameterValue("DisplayCulture"); + _isDisplayCulture = !(string.IsNullOrWhiteSpace(displayCulture)); + _initializedCulture = true; + return _isDisplayCulture; + } + else + { + return _isDisplayCulture; + } + } + } + + protected virtual bool UseDefaultSourceFormat + { + get + { + if (_initializedSource) + { + return _useDefaultSourceFormat; + } + // check if a user specifies -source and its source name >= the default width. if so, we will widen the source column + if (!_resolvedUserSpecifiedSource.IsNullOrEmpty()) + { + //the default width of the Source column is 16 + _useDefaultSourceFormat = _resolvedUserSpecifiedSource.Any(each => each.Source.Length <= 16); + } + _initializedSource = true; + return _useDefaultSourceFormat; + } + } + private string GetSoftwareIdentityTypeName(SoftwareIdentity package) + { + if(_initializedTypeName) + { + return _softwareIdentityTypeName; + } + + //check if a user specifies -source with a long source name such as http://www.powershellgallery.com/api/v2, + // if so we will choose the longsource column format. + if (!UseDefaultSourceFormat) + { + _softwareIdentityTypeName += "#DisplayLongSourceName"; + _hasTypeNameChanged = true; + } + //provider has the "DisplayCulture" in the Get-DynamicOption() + if (IsDisplayCulture) + { + _softwareIdentityTypeName += "#DisplayCulture"; + _hasTypeNameChanged = true; + } + + //provider defines the 'DisplayLongName' feature in the Get-Feature() + if (package != null && (package.Provider != null && (package.Provider.Features != null && package.Provider.Features.ContainsKey("DisplayLongName")))) + { + _softwareIdentityTypeName += "#DisplayLongName"; + _hasTypeNameChanged = true; + } + + _initializedTypeName = true; + + return _softwareIdentityTypeName; + } + + /// + /// This method is used for customizing the format of the OneGet *-Package output display. + /// + /// + /// + protected object AddPropertyToSoftwareIdentity(SoftwareIdentity package) + { + // Use the default output format if a user does not provide the -providername property, e.g. find-package + if (package == null || (!_isUserSpecifyOneProviderName && !IsDisplayCulture && UseDefaultSourceFormat)) + { + return package; + } + + // Customize the output format + var typeName = GetSoftwareIdentityTypeName(package); + + // For the find-package -providername nuget case, we can return right away. + if (!_hasTypeNameChanged) { + return package; + } + + var swidTagAsPsobj = PSObject.AsPSObject(package); + var noteProperty = new PSNoteProperty("PropertyOfSoftwareIdentity", "PropertyOfSoftwareIdentity"); + swidTagAsPsobj.Properties.Add(noteProperty, true); + swidTagAsPsobj.TypeNames.Insert(0, typeName); + return swidTagAsPsobj; + } + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/CmdletWithSearch.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/CmdletWithSearch.cs new file mode 100644 index 000000000..fe0cb50bc --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/CmdletWithSearch.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets { + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Management.Automation; + using Microsoft.PackageManagement.Internal.Packaging; + + public abstract class CmdletWithSearch : CmdletWithProvider { + protected CmdletWithSearch(OptionCategory[] categories) + : base(categories) { + } + + protected override IEnumerable ParameterSets { + get { + return new[] {""}; + } + } + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + [Parameter(Position = 0)] + public virtual string[] Name {get; set;} + + [Parameter] + public virtual string RequiredVersion {get; set;} + + [Parameter] + public virtual string MinimumVersion {get; set;} + + [Parameter] + public virtual string MaximumVersion {get; set;} + + [Parameter] + public virtual SwitchParameter AllVersions { get; set; } + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/CmdletWithSearchAndSource.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/CmdletWithSearchAndSource.cs new file mode 100644 index 000000000..260542f61 --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/CmdletWithSearchAndSource.cs @@ -0,0 +1,745 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets { + using System; + using System.Globalization; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Linq; + using System.Management.Automation; + using System.Management.Automation.Language; + using System.Security; + using System.Threading; + using Microsoft.PackageManagement.Implementation; + using Microsoft.PackageManagement.Internal.Api; + using Microsoft.PackageManagement.Internal.Implementation; + using Microsoft.PackageManagement.Internal.Packaging; + using Microsoft.PackageManagement.Internal.Utility.Async; + using Microsoft.PackageManagement.Internal.Utility.Collections; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Packaging; + using Utility; + using CollectionExtensions = Microsoft.PackageManagement.Internal.Utility.Extensions.CollectionExtensions; + using Constants = PackageManagement.Constants; + using DictionaryExtensions = Microsoft.PackageManagement.Internal.Utility.Extensions.DictionaryExtensions; + using Directory = System.IO.Directory; + using FilesystemExtensions = Microsoft.PackageManagement.Internal.Utility.Extensions.FilesystemExtensions; + + public abstract class CmdletWithSearchAndSource : CmdletWithSearch { + internal readonly OrderedDictionary> _resultsPerName = new OrderedDictionary>(); + protected List _providersNotFindingAnything = new List(); + internal static readonly string[] ProviderFilters = new[] { "Packagemanagement", "Provider" }; + protected const string Bootstrap = "Bootstrap"; + protected const string PowerShellGet = "PowerShellGet"; + internal static readonly string[] RequiredProviders = new[] { Bootstrap, PowerShellGet }; + private readonly HashSet _sourcesTrusted = new HashSet(); + private readonly HashSet _sourcesDeniedTrust = new HashSet(); + private bool _yesToAll = false; + private bool _noToAll = false; + + protected CmdletWithSearchAndSource(OptionCategory[] categories) + : base(categories) { + } + + private string[] _sources; + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "It's a performance thing.")] + [Parameter(ValueFromPipelineByPropertyName = true)] + public virtual string[] Source + { + get + { + return _sources; + } + set + { + _sources = value; + + if (_sources != null && _sources.Length != 0) { + ProviderInfo providerInfo; + + _sources = _sources.SelectMany(source => { + if (source.Equals(".")) { + source = ".\\"; + } + //Need to resolve the path created via psdrive. + //e.g., New-PSDrive -Name x -PSProvider FileSystem -Root \\foobar\myfolder. Here we are resolving x:\ + try + { + if (FilesystemExtensions.LooksLikeAFilename(source)) + { + var resolvedPaths = GetResolvedProviderPathFromPSPath(source, out providerInfo); + + // Ensure the path is a single path from the file system provider + if ((providerInfo != null) && (resolvedPaths.Count == 1) && String.Equals(providerInfo.Name, "FileSystem", StringComparison.OrdinalIgnoreCase)) + { + return resolvedPaths[0].SingleItemAsEnumerable(); + } + } + } + catch (Exception) + { + //allow to continue handling the cases other than file system + } + + return source.SingleItemAsEnumerable(); + }).ToArray(); + } + } + } + + + [Parameter] + public virtual PSCredential Credential {get; set;} + + [Parameter] + [ValidateNotNull()] + public Uri Proxy { get; set; } + + [Parameter] + [ValidateNotNull()] + public PSCredential ProxyCredential { get; set; } + + public override IEnumerable Sources + { + get + { + if (CollectionExtensions.IsNullOrEmpty(Source)) { + return Microsoft.PackageManagement.Internal.Constants.Empty; + } + + return Source; + } + } + + /* + protected override IEnumerable SelectedProviders { + get { + // filter on provider names - if they specify a provider name, narrow to only those provider names. + var providers = SelectProviders(ProviderName).ReEnumerable(); + + // filter out providers that don't have the sources that have been specified (only if we have specified a source!) + if (Source != null && Source.Length > 0) { + // sources must actually match a name or location. Keeps providers from being a bit dishonest + + var potentialSources = providers.SelectMany(each => each.ResolvePackageSources(this.SuppressErrorsAndWarnings(IsProcessing)).Where(source => Source.ContainsAnyOfIgnoreCase(source.Name, source.Location))).ReEnumerable(); + + // prefer registered sources + var registeredSources = potentialSources.Where(source => source.IsRegistered).ReEnumerable(); + + providers = registeredSources.Any() ? registeredSources.Select(source => source.Provider).Distinct().ReEnumerable() : potentialSources.Select(source => source.Provider).Distinct().ReEnumerable(); + } + // filter on: dynamic options - if they specify any dynamic options, limit the provider set to providers with those options. + return FilterProvidersUsingDynamicParameters(providers).ToArray(); + } + } + */ + + /// + /// Returns web proxy that provider can use + /// Construct the webproxy using InternalWebProxy + /// + public override System.Net.IWebProxy WebProxy + { + get + { + if (Proxy != null) + { + return new PackageManagement.Utility.InternalWebProxy(Proxy, ProxyCredential == null ? null : ProxyCredential.GetNetworkCredential()); + } + + return null; + } + } + + public override string CredentialUsername + { + get + { + return Credential != null ? Credential.UserName : null; + } + } + + public override SecureString CredentialPassword + { + get + { + return Credential != null ? Credential.Password : null; + } + } + + public override bool ProcessRecordAsync() { + // record the names in the collection. + if (!CollectionExtensions.IsNullOrEmpty(Name)) { + foreach (var name in Name) { + DictionaryExtensions.GetOrAdd(_resultsPerName, name, () => null); + } + } + + // it's searching for packages. + // we need to do the actual search for the packages now, + // and hold the results until EndProcessingAsync() + // where we can determine if we we have no ambiguity left. + SearchForPackages(); + + return true; + } + + + + private MutableEnumerable FindFiles(string path) { + if (FilesystemExtensions.LooksLikeAFilename(path)) { + ProviderInfo providerInfo; + var paths = GetResolvedProviderPathFromPSPath(path, out providerInfo).ReEnumerable(); + return + paths.SelectMany( + each => FilesystemExtensions.FileExists(each) ? CollectionExtensions.SingleItemAsEnumerable(each) : FilesystemExtensions.DirectoryExists(each) ? Directory.GetFiles(each) : Microsoft.PackageManagement.Internal.Constants.Empty) + .ReEnumerable(); + } + return Microsoft.PackageManagement.Internal.Constants.Empty.ReEnumerable(); + } + + + protected bool SpecifiedMinimumOrMaximum + { + get + { + return !string.IsNullOrWhiteSpace(MaximumVersion) || !string.IsNullOrWhiteSpace(MinimumVersion); + } + } + + private List _uris = new List(); + private Dictionary, byte[]>> _files = new Dictionary, byte[]>>(StringComparer.OrdinalIgnoreCase); + + private IEnumerable _names; + + private bool IsUri(string name) { + Uri packageUri; + if (Uri.TryCreate(name, UriKind.Absolute, out packageUri)) { + // if it's an uri, then we search via uri or file! + if (!packageUri.IsFile) { + _uris.Add(packageUri); + return true; + } + } + return false; + } + + + private bool IsFile(string name) { + var files = FindFiles(name); + if (files.Any()) { + foreach (var f in files) { + if (_files.ContainsKey(f)) { + // if we've got this file already by another parameter, just update it to + // keep track that we've somehow got it twice. + _files[f].Item1.Add(name); + } else { + // otherwise, lets' grab the first chunk of this file so we can check what providers + // can handle it (later) + DictionaryExtensions.Add(_files, f, new List { + name + }, FilesystemExtensions.ReadBytes(f, 1024)); + } + } + + return true; + } + return false; + } + + protected virtual IHostApi GetProviderSpecificOption(PackageProvider pv) { + return this.ProviderSpecific(pv); + } + + protected virtual bool EnsurePackageIsProvider(SoftwareIdentity package) { + return true; + } + + + protected override string BootstrapNuGet + { + get + { + //find, install, save- inherits from this class. They all need bootstrap NuGet if does not exists. + return "true"; + } + } + /// + /// Validate if the package is a provider package. + /// + /// + /// + protected bool ValidatePackageProvider(SoftwareIdentity package) { + //no need to filter on the packages from the bootstrap site as only providers will be published out there. + if (package.ProviderName.EqualsIgnoreCase("Bootstrap")) { + return true; + } + + //get the tags info from the package's swid + var tags = package.Metadata["tags"].ToArray(); + + //Check if the provider has provider tags + var found = false; + foreach (var filter in ProviderFilters) { + found = false; + if (tags.Any(tag => tag.ContainsIgnoreCase(filter))) { + found = true; + } else { + break; + } + } + return found; + } + + private void ProcessRequests(PackageProvider[] providers) + { + if (providers == null || providers.Length == 0) + { + return; + } + + var requests = providers.SelectMany(pv => { + Verbose(Resources.Messages.SelectedProviders, pv.ProviderName); + // for a given provider, if we get an error, we want just that provider to stop. + var host = GetProviderSpecificOption(pv); + + var a = _uris.Select(uri => new { + query = new List { + uri.AbsolutePath + }, + provider = pv, + packages = pv.FindPackageByUri(uri, host).CancelWhen(CancellationEvent.Token) + }); + + var b = _files.Keys.Where(file => pv.IsSupportedFile(_files[file].Item2)).Select(file => new { + query = _files[file].Item1, + provider = pv, + packages = pv.FindPackageByFile(file, host) + }); + + var c = _names.Select(name => new { + query = new List { + name + }, + provider = pv, + packages = pv.FindPackage(name, RequiredVersion, MinimumVersion, MaximumVersion, (Sources != null && Sources.Count() > 1) ? host.SuppressErrorsAndWarnings(IsProcessing) : host) + }); + + return a.Concat(b).Concat(c); + }).ToArray(); + + Debug("Calling SearchForPackages After Select {0}", requests.Length); + + if (AllVersions || !SpecifiedMinimumOrMaximum) { + // the user asked for every version or they didn't specify any version ranges + // either way, that means that we can just return everything that we're finding. + + while (WaitForActivity(requests.Select(each => each.packages))) { + + // keep processing while any of the the queries is still going. + + foreach (var result in requests.Where(each => each.packages.HasData)) { + // look only at requests that have data waiting. + + foreach (var package in result.packages.GetConsumingEnumerable()) { + // process the results for that set. + // check if the package is a provider package. If so we need to filter on the packages for the providers. + if (EnsurePackageIsProvider(package)) { + ProcessPackage(result.provider, result.query, package); + } + } + } + + requests = requests.FilterWithFinalizer(each => each.packages.IsConsumed, each => each.packages.Dispose()).ToArray(); + } + + + } else { + // now this is where it gets a bit funny. + // the user specified a min or max + // and so we have to only return the highest one in the set for a given package. + + while (WaitForActivity(requests.Select(each => each.packages))) { + // keep processing while any of the the queries is still going. + foreach (var perProvider in requests.GroupBy(each => each.provider)) { + foreach (var perQuery in perProvider.GroupBy(each => each.query)) { + if (perQuery.All(each => each.packages.IsCompleted && !each.packages.IsConsumed)) { + foreach (var pkg in from p in perQuery.SelectMany(each => each.packages.GetConsumingEnumerable()) + group p by new { + p.Name, + p.Source + } + // for a given name + into grouping + // get the latest version only + select grouping.OrderByDescending(pp => pp, SoftwareIdentityVersionComparer.Instance).First()) { + + if (EnsurePackageIsProvider(pkg)) { + ProcessPackage(perProvider.Key, perQuery.Key, pkg); + } + } + } + } + } + // filter out whatever we're done with. + requests = requests.FilterWithFinalizer(each => each.packages.IsConsumed, each => each.packages.Dispose()).ToArray(); + } + } + + // dispose of any requests that didn't get cleaned up earlier. + foreach (var i in requests) { + i.packages.Dispose(); + } + } + + protected virtual void SearchForPackages() { + try { + + var providers = SelectedProviders.ToArray(); + + // filter the items into three types of searches + _names = CollectionExtensions.IsNullOrEmpty(Name) ? CollectionExtensions.SingleItemAsEnumerable(string.Empty) : Name.Where(each => !IsUri(each) && !IsFile(each)).ToArray(); + + foreach (var n in _names) { + Debug("Calling SearchForPackages. Name='{0}'", n); + } + + ProcessRequests(providers.Where(pv => string.Equals(pv.ProviderName, "bootstrap", StringComparison.OrdinalIgnoreCase)).ToArray()); + + ProcessRequests(providers.Where(pv => !string.Equals(pv.ProviderName, "bootstrap", StringComparison.OrdinalIgnoreCase)).ToArray()); + + } catch (Exception ex) { + + Debug(ex.ToString()); + } + } + + protected virtual void ProcessPackage(PackageProvider provider, IEnumerable searchKey, SoftwareIdentity package) { + foreach (var key in searchKey) { + + _resultsPerName.GetOrSetIfDefault(key, () => new List()).Add(package); + } + } + + protected bool CheckUnmatchedPackages() { + var unmatched = _resultsPerName.Keys.Where(each => _resultsPerName[each] == null).ReEnumerable(); + var result = true; + + if (unmatched.Any()) { + // whine about things not matched. + foreach (var name in unmatched) { + + Debug(string.Format(CultureInfo.CurrentCulture, "unmatched package name='{0}'", name)); + + if (name == string.Empty) { + // no name + result = false; + Error(Constants.Errors.NoPackagesFoundForProvider, _providersNotFindingAnything.Select(each => each.ProviderName).JoinWithComma()); + } else { + if (WildcardPattern.ContainsWildcardCharacters(name)) { + Verbose(Constants.Messages.NoMatchesForWildcard, name); + result = false; // even tho' it's not an 'error' it is still enough to know not to actually install. + } else { + result = false; + NonTerminatingError(Constants.Errors.NoMatchFoundForCriteria, name); + } + } + } + } + + return result; + } + + protected IEnumerable CheckMatchedDuplicates() { + // if there are overmatched packages we need to know why: + // are they found across multiple providers? + // are they found accross multiple sources? + // are they all from the same source? + + foreach (var list in _resultsPerName.Values.Where(each => each != null && each.Any())) { + if (list.Count == 1) { + //no overmatched + yield return list.FirstOrDefault(); + } else { + //process the overmatched case + SoftwareIdentity selectedPackage = null; + + var providers = list.Select(each => each.ProviderName).Distinct().ToArray(); + var sources = list.Select(each => each.Source).Distinct().ToArray(); + + //case: a user specifies -Source and multiple packages are found. In this case to determine which one should be installed, + // We treat the user's package source array is in a priority order, i.e. the first package source has the highest priority. + // Of course, these packages should not be from a single source with the same provider. + // Example: install-package -Source @('src1', 'src2') + // install-package -Source @('src1', 'src2') -Provider @('p1', 'p2') + if (Sources.Any() && (providers.Length != 1 || sources.Length != 1)) { + // let's use the first source as our priority.As long as we find a package, we exit the 'for' loop righ away + foreach (var source in Sources) { + //select all packages matched source + var pkgs = list.Where(package => source.EqualsIgnoreCase(package.Source) || (UserSpecifiedSourcesList.Keys.ContainsIgnoreCase(package.Source) && source.EqualsIgnoreCase(UserSpecifiedSourcesList[package.Source]))).ToArray(); + if (pkgs.Length == 0) { + continue; + } + if (pkgs.Length == 1) { + //only one provider found the package + selectedPackage = pkgs[0]; + break; + } + if (ProviderName == null) { + //user does not specify '-providerName' but we found multiple packages with a source, can not determine which one + //will error out + break; + } + if (pkgs.Length > 1) { + //case: multiple providers matched the same source. + //need to process provider's priority order + var pkg = ProviderName.Select(p => pkgs.FirstOrDefault(each => each.ProviderName.EqualsIgnoreCase(p))).FirstOrDefault(); + if (pkg != null) { + selectedPackage = pkg; + break; + } + } + }//inner foreach + + //case: a user specifies -Provider array but no -Source and multiple packages are found. In this case to determine which one should be installed, + // We treat the user's package provider array is in a priority order, i.e. the first provider in the array has the highest priority. + // Of course, these packages should not be from a single source with the same provider. + // Example: install-package -Provider @('p1', 'p2') + } else if (ProviderName != null && ProviderName.Any() && (providers.Length != 1 || sources.Length != 1)) { + foreach (var providerName in ProviderName) { + + //select all packages matched with the provider name + var packages = list.Where(each => providerName.EqualsIgnoreCase(each.ProviderName)).ToArray(); + if (packages.Length == 0) { + continue; + } + if (packages.Length == 1) { + //only one provider found the package, that's good + selectedPackage = packages[0]; + break; + } else { + //user does not specify '-source' but we found multiple packages with one provider, we can not determine which one + //will error out + break; + } + } + } + + if (selectedPackage != null) { + yield return selectedPackage; + } else { + //error out for the overmatched case + var suggestion = ""; + if (providers.Length == 1) { + // it's matching this package multiple times in the same provider. + if (sources.Length == 1) { + // matching it from a single source. + // be more exact on matching name? or version? + suggestion = Resources.Messages.SuggestRequiredVersion; + } else { + // it's matching the same package from multiple sources + // tell them to use -source + suggestion = Resources.Messages.SuggestSingleSource; + } + } else { + // found across multiple providers + // must specify -provider + suggestion = Resources.Messages.SuggestSingleProviderName; + } + + string searchKey = null; + + foreach (var pkg in list) { + + Warning(Constants.Messages.MatchesMultiplePackages, pkg.SearchKey, pkg.ProviderName, pkg.Name, pkg.Version, GetPackageSourceNameOrLocation(pkg)); + searchKey = pkg.SearchKey; + } + Error(Constants.Errors.DisambiguateForInstall, searchKey, GetMessageString(suggestion, suggestion)); + } + } + } + } + + // Performs a reverse lookup from Package Source Location -> Source Name for a Provider + private string GetPackageSourceNameOrLocation(SoftwareIdentity package) + { + // Default to Source Location Url + string packageSourceName = package.Source; + + // Get the package provider object + var packageProvider = SelectProviders(package.ProviderName).ReEnumerable(); + + if (!packageProvider.Any()) + { + return packageSourceName; + } + + + // For any issues with reverse lookup (SourceLocation -> SourceName), return Source Location Url + try + { + var packageSource = packageProvider.Select(each => each.ResolvePackageSources(this.SuppressErrorsAndWarnings(IsProcessing)).Where(source => source.IsRegistered && (source.Location.EqualsIgnoreCase(package.Source)))).ReEnumerable(); + + if (packageSource.Any()) { + var pkgSource = packageSource.FirstOrDefault(); + if (pkgSource != null) { + var source = pkgSource.FirstOrDefault(); + if (source != null) { + packageSourceName = source.Name; + } + } + } + } + catch (Exception e) + { + e.Dump(); + } + + return packageSourceName; + } + + protected bool InstallPackages(params SoftwareIdentity[] packagesToInstall) { + + if(packagesToInstall == null || packagesToInstall.Length == 0) + { + return false; + } + // first, check to see if we have all the required dynamic parameters + // for each package/provider + foreach (var package in packagesToInstall) { + var pkg = package; + foreach (var parameter in DynamicParameterDictionary.Values.OfType() + .Where(param => param.IsSet == false && param.Options.Any(option => option.ProviderName == pkg.ProviderName && option.Category == OptionCategory.Install && option.IsRequired))) { + // this is not good. there is a required parameter for the package + // and the user didn't specify it. We should return the error to the user + // and they can try again. + Error(Constants.Errors.PackageInstallRequiresOption, package.Name, package.ProviderName, parameter.Name); + Cancel(); + } + } + + if (IsCanceled) { + return false; + } + var progressId = 0; + var triedInstallCount = 0; + + if (packagesToInstall.Length > 1) { + progressId = StartProgress(0, Constants.Messages.InstallingPackagesCount, packagesToInstall.Length); + } + var n = 0; + + foreach (var pkg in packagesToInstall) { + if (packagesToInstall.Length > 1) { + Progress(progressId, (n*100/packagesToInstall.Length) + 1, Constants.Messages.InstallingPackageMultiple, pkg.Name, ++n, packagesToInstall.Length); + } + var provider = SelectProviders(pkg.ProviderName).FirstOrDefault(); + if (provider == null) { + Error(Constants.Errors.UnknownProvider, pkg.ProviderName); + return false; + } + + // quickly check to see if this package is already installed. + var installedPkgs = provider.GetInstalledPackages(pkg.Name, pkg.Version, null, null, this.ProviderSpecific(provider)).CancelWhen(CancellationEvent.Token).ToArray(); + if (IsCanceled) { + // if we're stopping, just get out asap. + return false; + } + + // todo: this is a terribly simplistic way to do this, we'd better rethink this soon + if (!Force) { + if (installedPkgs.Any(each => each.Name.EqualsIgnoreCase(pkg.Name) && each.Version.EqualsIgnoreCase(pkg.Version))) { + // it looks like it's already installed. + // skip it. + Verbose(Constants.Messages.SkippedInstalledPackage, pkg.Name, pkg.Version); + + if (packagesToInstall.Length > 1) { + Progress(progressId, (n*100/packagesToInstall.Length) + 1, Constants.Messages.SkippedInstalledPackageMultiple, pkg.Name, n, packagesToInstall.Length); + } + continue; + } + } + + + try { + if (ShouldProcessPackageInstall(pkg.Name, pkg.Version, pkg.Source)) { + foreach (var installedPkg in provider.InstallPackage(pkg, this).CancelWhen(CancellationEvent.Token)) { + if (IsCanceled) { + // if we're stopping, just get out asap. + return false; + } + + WriteObject(AddPropertyToSoftwareIdentity(installedPkg)); + LogEvent(EventTask.Install, EventId.Install, + Resources.Messages.PackageInstalled, + installedPkg.Name, + installedPkg.Version, + installedPkg.ProviderName, + installedPkg.Source ?? string.Empty, + installedPkg.Status ?? string.Empty, + installedPkg.InstallationPath ?? string.Empty); + TraceMessage(Constants.InstallPackageTrace, installedPkg); + triedInstallCount++; + } + } + } catch (Exception e) { + e.Dump(); + Error(Constants.Errors.InstallationFailure, pkg.Name); + return false; + } + if (packagesToInstall.Length > 1) { + Progress(progressId, (n*100/packagesToInstall.Length) + 1, Constants.Messages.InstalledPackageMultiple, pkg.Name, n, packagesToInstall.Length); + } + + } + + return (triedInstallCount == packagesToInstall.Length); + } + + protected bool ShouldProcessPackageInstall(string packageName, string version, string source) { + try { + //-force is already handled before reaching this stage + return ShouldProcess(FormatMessageString(Constants.Messages.TargetPackage, packageName, version, source), FormatMessageString(Constants.Messages.ActionInstallPackage)).Result; + } catch { + } + return false; + } + + public override bool ShouldContinueWithUntrustedPackageSource(string package, string packageSource) { + try { + if (_sourcesTrusted.Contains(packageSource) || Force || WhatIf || _yesToAll) { + return true; + } + if (_sourcesDeniedTrust.Contains(packageSource) || _noToAll) { + return false; + } + var shouldContinueResult = ShouldContinue(FormatMessageString(Constants.Messages.QueryInstallUntrustedPackage, package, packageSource), FormatMessageString(Constants.Messages.CaptionSourceNotTrusted), true).Result; + _yesToAll = shouldContinueResult.yesToAll; + _noToAll = shouldContinueResult.noToAll; + if (shouldContinueResult.result) { + _sourcesTrusted.Add(packageSource); + return true; + } else { + _sourcesDeniedTrust.Add(packageSource); + } + } catch { + } + return false; + } + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/CmdletWithSource.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/CmdletWithSource.cs new file mode 100644 index 000000000..3fd91f1ac --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/CmdletWithSource.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets { + using System.Diagnostics.CodeAnalysis; + using System.Management.Automation; + using System.Security; + using Microsoft.PackageManagement.Internal.Packaging; + using Microsoft.PackageManagement.Packaging; + + public abstract class CmdletWithSource : CmdletWithProvider { + protected CmdletWithSource(OptionCategory[] categories) + : base(categories) { + } + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + [Parameter(ParameterSetName = Constants.ParameterSets.SourceByInputObjectSet, Mandatory = true, ValueFromPipeline = true)] + public PackageSource[] InputObject {get; set;} + + [Parameter] + public PSCredential Credential {get; set;} + + public override string CredentialUsername { + get { + return Credential != null ? Credential.UserName : null; + } + } + + public override SecureString CredentialPassword { + get { + return Credential != null ? Credential.Password : null; + } + } + + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/FindPackage.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/FindPackage.cs new file mode 100644 index 000000000..c85a89a62 --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/FindPackage.cs @@ -0,0 +1,99 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Management.Automation; + using Microsoft.PackageManagement.Implementation; + using Microsoft.PackageManagement.Internal.Implementation; + using Microsoft.PackageManagement.Internal.Packaging; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Packaging; + using Utility; + + [Cmdlet(VerbsCommon.Find, Constants.Nouns.PackageNoun, HelpUri = "http://go.microsoft.com/fwlink/?LinkID=517132"), OutputType(new Type[1] { typeof(SoftwareIdentity) })] + public sealed class FindPackage : CmdletWithSearchAndSource { + public FindPackage() + : base(new[] { + OptionCategory.Provider, OptionCategory.Source, OptionCategory.Package + }) { + } + + protected override IEnumerable ParameterSets { + get { + return new[] {"",}; + } + } + + [Parameter] + public SwitchParameter IncludeDependencies {get; set;} + + [Parameter] + public override SwitchParameter AllVersions {get; set;} + + public override bool BeginProcessingAsync() + { + if (!string.IsNullOrEmpty(RequiredVersion)) + { + if ((!string.IsNullOrEmpty(MinimumVersion)) || (!string.IsNullOrEmpty(MaximumVersion))) + { + Error(Constants.Errors.VersionRangeAndRequiredVersionCannotBeSpecifiedTogether); + } + } + + return true; + } + + protected override void ProcessPackage(PackageProvider provider, IEnumerable searchKey, SoftwareIdentity package) { + + try { + + base.ProcessPackage(provider, searchKey, package); + + // return the object to the caller now. + WriteObject(AddPropertyToSoftwareIdentity(package)); + + if (IncludeDependencies) { + var missingDependencies = new HashSet(); + foreach (var dep in package.Dependencies) { + // note: future work may be needed if the package sources currently selected by the user don't + // contain the dependencies. + var dependendcies = PackageManagementService.FindPackageByCanonicalId(dep, this); + var depPkg = dependendcies.OrderByDescending(pp => pp, SoftwareIdentityVersionComparer.Instance).FirstOrDefault(); + + if (depPkg == null) { + missingDependencies.Add(dep); + Warning(Constants.Messages.UnableToFindDependencyPackage, dep); + } else { + ProcessPackage(depPkg.Provider, searchKey.Select(each => each + depPkg.Name).ToArray(), depPkg); + } + } + if (missingDependencies.Any()) { + Error(Constants.Errors.UnableToFindDependencyPackage, missingDependencies.JoinWithComma()); + } + } + } catch (Exception ex) { + + Debug("Calling ProcessPackage {0}", ex.ToString()); + } + } + + + public override bool EndProcessingAsync() { + return CheckUnmatchedPackages(); + } + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/FindPackageProvider.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/FindPackageProvider.cs new file mode 100644 index 000000000..dde4ed669 --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/FindPackageProvider.cs @@ -0,0 +1,189 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Management.Automation; + using Microsoft.PackageManagement.Implementation; + using Microsoft.PackageManagement.Internal.Api; + using Microsoft.PackageManagement.Internal.Packaging; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Internal.Utility.Plugin; + using Microsoft.PackageManagement.Packaging; + using Utility; + + [Cmdlet("Find", Constants.Nouns.PackageProviderNoun, HelpUri = "http://go.microsoft.com/fwlink/?LinkId=626939"), OutputType(new Type[1] {typeof(SoftwareIdentity)})] + public sealed class FindPackageProvider : CmdletWithSearchAndSource { + private const string FilterOnTag = "FilterOnTag"; + + public FindPackageProvider() : base(new[] {OptionCategory.Package}) { + //this will include bootstrap provider + ShouldSelectAllProviders = true; + } + + [Parameter(ValueFromPipelineByPropertyName = true, Position = 0)] + public override string[] Name { get; set; } + + [Parameter] + public override SwitchParameter AllVersions { get; set; } + + [Parameter(ValueFromPipelineByPropertyName = true)] + public override string[] Source + { + get + { + return base.Source; + } + set + { + base.Source = value; + } + } + + [Parameter] + public SwitchParameter IncludeDependencies { get; set; } + + protected override void GenerateCmdletSpecificParameters(Dictionary unboundArguments) { + //this will supress the dynamic parameters from the parent classes + //Noop + } + + + protected override DynamicOption[] CachedDynamicOptions + { + //this will suppress the dynamic parameters from parent classes + get + { + return new[] {new DynamicOption()}; + } + } + + public override bool ProcessRecordAsync() { + ValidateVersion(RequiredVersion); + ValidateVersion(MinimumVersion); + ValidateVersion(MaximumVersion); + + if (RequiredVersion != null) { + if ((!string.IsNullOrWhiteSpace(MaximumVersion)) || (!string.IsNullOrWhiteSpace(MinimumVersion))) { + Error(Constants.Errors.VersionRangeAndRequiredVersionCannotBeSpecifiedTogether); + return false; + } + } + //Error out for the case where multiple provider names with any version specififed + if (Name.IsNullOrEmpty()) { + if ((!string.IsNullOrWhiteSpace(RequiredVersion)) || (!string.IsNullOrWhiteSpace(MinimumVersion)) || (!string.IsNullOrWhiteSpace(MaximumVersion))) { + Error(Constants.Errors.MultipleNamesWithVersionNotAllowed); + return false; + } + } else { + if (((Name.Length > 1) || Name.Any(each => each.ContainsWildcards())) && + ((!string.IsNullOrWhiteSpace(RequiredVersion)) || (!string.IsNullOrWhiteSpace(MinimumVersion)) || (!string.IsNullOrWhiteSpace(MaximumVersion)))) { + Error(Constants.Errors.MultipleNamesWithVersionNotAllowed); + return false; + } + } + if (AllVersions.IsPresent) { + if ((!string.IsNullOrWhiteSpace(RequiredVersion)) || (!string.IsNullOrWhiteSpace(MinimumVersion)) || (!string.IsNullOrWhiteSpace(MaximumVersion))) { + Error(Constants.Errors.AllVersionsCannotBeUsedWithOtherVersionParameters); + return false; + } + } + + base.ProcessRecordAsync(); + + return true; + } + + /// + /// Select existing providers on the system that will be used for searching any package providers in the repositories. + /// + protected override IEnumerable SelectedProviders + { + get + { + var availableProviders = base.SelectedProviders.ToArray(); + if (availableProviders.Any(each => RequiredProviders.ContainsAnyOfIgnoreCase(each.ProviderName))) + { + return availableProviders.Where(each => RequiredProviders.ContainsIgnoreCase(each.ProviderName)); + } + //if the existing available providers don't have what we required, error out + Error(Constants.Errors.RegisterPackageSourceRequired, Source.JoinWithComma()); + return Enumerable.Empty(); + } + } + + protected override IHostApi GetProviderSpecificOption(PackageProvider pv) + { + var host = this.ProviderSpecific(pv); + var host1 = host; + //add filterontag for finding providers. Provider keys are: PackageManagment and Provider + host = host.Extend( + new + { + GetOptionValues = new Func>(key => + { + if (key.EqualsIgnoreCase(FilterOnTag)) { + return ProviderFilters; + } + + return host1.GetOptionValues(key); + }), + }); + return host; + } + + protected override bool EnsurePackageIsProvider(SoftwareIdentity package) { + //Make sure the package is a provider package + return ValidatePackageProvider(package); + } + + protected override void ProcessPackage(PackageProvider provider, IEnumerable searchKey, SoftwareIdentity package) { + + Debug("Calling ProcessPackage SearchKey = '{0}' and provider name ='{1}'", searchKey, package.ProviderName); + try { + base.ProcessPackage(provider, searchKey, package); + + // output to console + WriteObject(AddPropertyToSoftwareIdentity(package)); + + if (IncludeDependencies) { + var missingDependencies = new HashSet(); + foreach (var dep in package.Dependencies) { + var dependendcies = PackageManagementService.FindPackageByCanonicalId(dep, this); + var depPkg = dependendcies.OrderByDescending(pp => pp, SoftwareIdentityVersionComparer.Instance).FirstOrDefault(); + + if (depPkg == null) { + missingDependencies.Add(dep); + Warning(Constants.Messages.UnableToFindDependencyPackage, dep); + } else { + ProcessPackage(depPkg.Provider, searchKey.Select(each => each + depPkg.Name).ToArray(), depPkg); + } + } + if (missingDependencies.Any()) { + Error(Constants.Errors.UnableToFindDependencyPackage, missingDependencies.JoinWithComma()); + } + } + } catch (Exception ex) { + Debug("Calling ProcessPackage {0}", ex.Message); + } + } + + public override bool EndProcessingAsync() { + return CheckUnmatchedPackages(); + } + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/GetPackage.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/GetPackage.cs new file mode 100644 index 000000000..67e7c97da --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/GetPackage.cs @@ -0,0 +1,215 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets { + using System.Collections.Generic; + using System.Linq; + using System.Management.Automation; + using Utility; + using System.Diagnostics.CodeAnalysis; + using Microsoft.PackageManagement.Internal.Packaging; + using Microsoft.PackageManagement.Internal.Utility.Async; + using Microsoft.PackageManagement.Internal.Utility.Collections; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Packaging; + + [Cmdlet(VerbsCommon.Get, Constants.Nouns.PackageNoun, HelpUri = "http://go.microsoft.com/fwlink/?LinkID=517135")] + public class GetPackage : CmdletWithSearch { + private readonly Dictionary _namesProcessed = new Dictionary(); + private readonly string _newSoftwareIdentityTypeName = "Microsoft.PackageManagement.Packaging.SoftwareIdentity#GetPackage"; + + public GetPackage() + : base(new[] { + OptionCategory.Provider, OptionCategory.Install + }) { + } + + protected override IEnumerable ParameterSets { + get { + return new[] {""}; + } + } + + protected IEnumerable UnprocessedNames { + get { + return _namesProcessed.Keys.Where(each => !_namesProcessed[each]); + } + } + + [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Considered. Still No.")] + protected bool IsPackageInVersionRange(SoftwareIdentity pkg) { + if (RequiredVersion != null && SoftwareIdentityVersionComparer.CompareVersions(pkg.VersionScheme, pkg.Version, RequiredVersion) != 0) { + return false; + } + + if (MinimumVersion != null && SoftwareIdentityVersionComparer.CompareVersions(pkg.VersionScheme, pkg.Version, MinimumVersion) < 0) { + return false; + } + + if (MaximumVersion != null && SoftwareIdentityVersionComparer.CompareVersions(pkg.VersionScheme, pkg.Version, MaximumVersion) > 0) { + return false; + } + + return true; + } + + protected bool IsDuplicate(SoftwareIdentity package) { + // todo: add duplicate checking (need canonical ids) + return false; + } + + public override bool ProcessRecordAsync() { + + ValidateVersion(RequiredVersion); + ValidateVersion(MinimumVersion); + ValidateVersion(MaximumVersion); + + // If AllVersions is specified, make sure other version parameters are not supplied + if (AllVersions.IsPresent) + { + if ((!string.IsNullOrWhiteSpace(RequiredVersion)) || (!string.IsNullOrWhiteSpace(MinimumVersion)) || (!string.IsNullOrWhiteSpace(MaximumVersion))) + { + Error(Constants.Errors.AllVersionsCannotBeUsedWithOtherVersionParameters); + } + } + + // Cannot have Max/Min version parameters with RequiredVersion + if (RequiredVersion != null) + { + if ((!string.IsNullOrWhiteSpace(MaximumVersion)) || (!string.IsNullOrWhiteSpace(MinimumVersion))) + { + Error(Constants.Errors.VersionRangeAndRequiredVersionCannotBeSpecifiedTogether); + } + } + + // keep track of what package names the user asked for. + if (!Name.IsNullOrEmpty()) { + foreach (var name in Name) { + _namesProcessed.GetOrAdd(name, () => false); + } + } + + var requests = (Name.IsNullOrEmpty() ? + + // if the user didn't specify any names + SelectedProviders.Select(pv => new { + query = "?", + packages = pv.GetInstalledPackages("",RequiredVersion, MinimumVersion, MaximumVersion, this.ProviderSpecific(pv)).CancelWhen(CancellationEvent.Token) + }) : + + // if the user specified a name, + SelectedProviders.SelectMany(pv => { + // for a given provider, if we get an error, we want just that provider to stop. + var host = this.ProviderSpecific(pv); + + return Name.Select(name => new { + query = name, + packages = pv.GetInstalledPackages(name, RequiredVersion, MinimumVersion, MaximumVersion, host).CancelWhen(CancellationEvent.Token) + }); + })).ToArray(); + + var potentialPackagesToProcess = new System.Collections.ObjectModel.Collection(); + while (WaitForActivity(requests.Select(each => each.packages))) { + + // keep processing while any of the the queries is still going. + foreach (var result in requests.Where(each => each.packages.HasData)) { + // look only at requests that have data waiting. + + foreach (var package in result.packages.GetConsumingEnumerable()) { + // process the results for that set. + + if (IsPackageInVersionRange(package)) { + // it only counts if the package is in the range we're looking for. + + // mark down that we found something for that query + _namesProcessed.AddOrSet(result.query, true); + + // If AllVersions is specified, process the package immediately + if (AllVersions) + { + // Process the package immediately if -AllVersions are required + ProcessPackage(package); + } + else + { + // Save to perform post-processing to eliminate duplicate versions and group by Name + potentialPackagesToProcess.Add(package); + } + } + } + } + // just work with whatever is not yet consumed + requests = requests.FilterWithFinalizer(each => each.packages.IsConsumed, each => each.packages.Dispose()).ToArray(); + } // end of WaitForActivity() + + // Peform post-processing only if -AllVersions is not specified + if (!AllVersions) + { + // post processing the potential packages as we have to display only + // 1 package per name (note multiple versions of the same package may be installed) + // In general, it is good practice to show only the latest one. + + // However there are cases when the same package can be found by different providers. in that case, we will show + // the packages from different providers even through they have the same package name. This is important because uninstall-package + // inherts from get-package, so that when the first provider does not implement the uninstall-package(), such as Programs, others will + // perform the uninstall. + + //grouping packages by package name first + var enumerablePotentialPackages = from p in potentialPackagesToProcess + group p by p.Name + into grouping + select grouping.OrderByDescending(pp => pp, SoftwareIdentityVersionComparer.Instance); + + //each group of packages with the same name, return the first if the packages are from the same provider + foreach (var potentialPackage in enumerablePotentialPackages.Select(pp => (from p in pp + group p by p.ProviderName + into grouping + select grouping.OrderByDescending(each => each, SoftwareIdentityVersionComparer.Instance).First())).SelectMany(pkgs => pkgs.ToArray())) { + ProcessPackage(potentialPackage); + } + } + + return true; + } + + protected virtual void ProcessPackage(SoftwareIdentity package) { + // Check for duplicates + if (!IsDuplicate(package)) { + + // Display the SoftwareIdentity object in a format: Name, Version, Source and Provider + var swidTagAsPsobj = PSObject.AsPSObject(package); + var noteProperty = new PSNoteProperty("PropertyOfSoftwareIdentity", "PropertyOfSoftwareIdentity"); + swidTagAsPsobj.Properties.Add(noteProperty, true); + swidTagAsPsobj.TypeNames.Insert(0, _newSoftwareIdentityTypeName); + + WriteObject(swidTagAsPsobj); + } + } + + public override bool EndProcessingAsync() { + // give out errors for any package names that we don't find anything for. + foreach (var name in UnprocessedNames) { + Error(Constants.Errors.NoMatchFound, name); + } + return true; + } + + //use wide Source column for get-package + protected override bool UseDefaultSourceFormat { + get { + return false; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/GetPackageProvider.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/GetPackageProvider.cs new file mode 100644 index 000000000..787b2a36a --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/GetPackageProvider.cs @@ -0,0 +1,85 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets { + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Management.Automation; + using Microsoft.PackageManagement.Internal.Utility.Collections; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + + [Cmdlet(VerbsCommon.Get, Constants.Nouns.PackageProviderNoun, HelpUri = "http://go.microsoft.com/fwlink/?LinkID=517136")] + public sealed class GetPackageProvider : CmdletBase { + protected override IEnumerable ParameterSets { + get { + return new[] {"",}; + } + } + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + [Parameter(Position = 0)] + public string[] Name {get; set;} + + [Parameter] + public SwitchParameter ListAvailable {get; set;} + + public override bool ProcessRecordAsync() { + + if (Name.IsNullOrEmpty()) { + var providers = ListAvailable.IsPresent ? PackageManagementService.GetAvailableProviders(this, Name).ReEnumerable() : SelectProviders(Name).ReEnumerable(); + + var providerOrdered = providers.OrderBy(each => each.ProviderName); + // Get all available providers + foreach (var p in providerOrdered) { + WriteObject(p); + } + } else { + ProcessProvidersFilteredByName(); + } + + return true; + } + + private void ProcessProvidersFilteredByName() { + //Do not log error when a provider is not found in the list returned by SelectProviders(). This allows the searching continues + //in the list of providers that are not loaded. + ShouldLogError = false; + var notfound = new List(); + foreach (var name in Name) { + var providers = ListAvailable.IsPresent ? PackageManagementService.GetAvailableProviders(this, new[] {name}).ReEnumerable() : SelectProviders(name).ReEnumerable(); + if (providers.Any()) { + var providerOrdered = providers.OrderByDescending(each => each.ProviderName); + + foreach (var provider in providerOrdered) { + WriteObject(provider); + } + } else { + notfound.Add(name); + } + } + + //Error out if the specific provider is not found + if (notfound.Any()) { + if (notfound.Count == 1) { + Error(ListAvailable.IsPresent ? Constants.Errors.UnknownProvider : Constants.Errors.UnknownProviderFromActivatedList, notfound.FirstOrDefault()); + } else { + Error(ListAvailable.IsPresent ? Constants.Errors.UnknownProviders : Constants.Errors.UnknownProviderFromActivatedList, notfound.JoinWithComma()); + } + } + notfound.Clear(); + } + } +} + diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/GetPackageSource.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/GetPackageSource.cs new file mode 100644 index 000000000..63b09fce9 --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/GetPackageSource.cs @@ -0,0 +1,194 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets { + using System.Collections.Generic; + using System.Linq; + using System.Management.Automation; + using Microsoft.PackageManagement.Internal.Packaging; + using Microsoft.PackageManagement.Internal.Utility.Async; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Packaging; + using Utility; + + [Cmdlet(VerbsCommon.Get, Constants.Nouns.PackageSourceNoun, HelpUri = "http://go.microsoft.com/fwlink/?LinkID=517137")] + public sealed class GetPackageSource : CmdletWithProvider { + private readonly List _unregistered = new List(); + private bool _found; + private bool _noLocation; + private bool _noName; + private string _name; + private string _originalName; + + public GetPackageSource() + : base(new[] { + OptionCategory.Provider, OptionCategory.Source + }) { + } + + protected override IEnumerable ParameterSets { + get { + return new[] {""}; + } + } + + [Parameter(Position = 0)] + public string Name { + get { + return _name; + } + set { + _originalName = value; + if (!string.IsNullOrWhiteSpace(_originalName) && _originalName.ContainsWildcards()){ + //'Name' means package Source Name here. if we pass down the source name with any wildcard charaters, providers cannot resolve them. + //set it to "" here just as a user does not provide -Name. With that Get-PackageSource returns all and we'll filter on results. + _name = string.Empty; + } else { + _name = _originalName; + } + } + } + + [Parameter] + public string Location {get; set;} + + private IEnumerable _sources { + get { + if (!string.IsNullOrWhiteSpace(Name)) { + yield return Name; + } + + if (!string.IsNullOrWhiteSpace(Location)) { + yield return Location; + } + } + } + + public override IEnumerable Sources { + get { + return _sources; + } + } + + private bool WriteSources(IEnumerable sources) { + foreach (var source in sources) { + _found = true; + WriteObject(source); + } + return _found; + } + + public override bool ProcessRecordAsync() { + var noName = string.IsNullOrWhiteSpace(Name); + var noLocation = string.IsNullOrWhiteSpace(Location); + var noCriteria = noName && noLocation; + + // store the information if we've ever had a name or location + _noName = _noName || noName; + _noLocation = _noLocation || noLocation; + + foreach (var provider in SelectedProviders) { + if (Stopping) { + return false; + } + + using (var src = provider.ResolvePackageSources(this.SuppressErrorsAndWarnings(IsProcessing)).CancelWhen(CancellationEvent.Token)) { + var sources = src.Distinct(); + if (!string.IsNullOrWhiteSpace(_originalName) && _originalName.ContainsWildcards()) { + WriteSources(sources.Where(each => each.Name.IsWildcardMatch(_originalName))); + continue; + } + if (noCriteria) { + // no criteria means just return whatever we found + if (WriteSources(sources)) { + } + } else { + var all = sources.ToArray(); + var registered = all.Where(each => each.IsRegistered); + + if (noName) + { + // just location was specified + if (WriteSources(registered.Where(each => each.Location.EqualsIgnoreCase(Location)))) { + continue; + } + } + else if (noLocation) + { + // just name was specified + if (WriteSources(registered.Where(each => each.Name.EqualsIgnoreCase(Name)))) + { + continue; + } + } + else + { + // name and location were specified + if (WriteSources(registered.Where(each => each.Name.EqualsIgnoreCase(Name) && each.Location.EqualsIgnoreCase(Location)))) { + continue; + } + } + // we haven't returned anything to the user yet... + // hold on to the unregistered ones. Might need these at the end. + _unregistered.AddRangeLocked(all.Where(each => !each.IsRegistered)); + } + } + } + + return true; + } + + public override bool EndProcessingAsync() { + if (!_found) { + if (_noName && _noLocation) { + // no criteria means just return whatever we found + if (WriteSources(_unregistered)) { + return true; + } + Warning(Constants.Messages.SourceNotFoundNoCriteria); + return true; + } + + if (_noName) { + // just location was specified + if (WriteSources(_unregistered.Where(each => each.Location.EqualsIgnoreCase(Location)))) { + return true; + } + Warning(Constants.Messages.SourceNotFoundForLocation, Location); + return true; + } + + if (_noLocation) + { + // just name was specified + if (WriteSources(_unregistered.Where(each => each.Name.EqualsIgnoreCase(Name)))) + { + return true; + } + Warning(Constants.Messages.SourceNotFound, Name); + return true; + } + + // both Name and Location were specified + if (WriteSources(_unregistered.Where(each => each.Name.EqualsIgnoreCase(Name) && each.Location.EqualsIgnoreCase(Location)))) { + return true; + } + + Warning(Constants.Messages.SourceNotFoundForNameAndLocation, Name, Location); + return true; + } + return true; + } + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/ImportPackageProvider.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/ImportPackageProvider.cs new file mode 100644 index 000000000..aa1cdeeae --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/ImportPackageProvider.cs @@ -0,0 +1,123 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics.CodeAnalysis; + using System.Management.Automation; + using System.Linq; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Internal.Packaging; + + [Cmdlet("Import", Constants.Nouns.PackageProviderNoun, HelpUri = "http://go.microsoft.com/fwlink/?LinkId=626942")] + public sealed class ImportPackageProvider : CmdletBase { + + protected override IEnumerable ParameterSets { + get { + return new[] {""}; + } + } + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + [Parameter(ValueFromPipelineByPropertyName = true, Position = 0, Mandatory = true)] + public string[] Name { get; set; } + + [Parameter] + public string RequiredVersion {get; set;} + + [Parameter] + public string MinimumVersion {get; set;} + + [Parameter] + public string MaximumVersion {get; set;} + + public override bool BeginProcessingAsync() { + ValidateVersion(RequiredVersion); + ValidateVersion(MinimumVersion); + ValidateVersion(MaximumVersion); + + //Error out for the case where requiredVersion with with maximumVersion or minimumVersion + if (!string.IsNullOrWhiteSpace(RequiredVersion)) { + if (!string.IsNullOrWhiteSpace(MaximumVersion) || !string.IsNullOrWhiteSpace(MinimumVersion)) { + Error(Constants.Errors.VersionRangeAndRequiredVersionCannotBeSpecifiedTogether); + return false; + } + } + + if (!string.IsNullOrWhiteSpace(MaximumVersion) && !string.IsNullOrWhiteSpace(MinimumVersion) + && new Version(MaximumVersion) < new Version(MinimumVersion)) { + Error(Constants.Errors.MinimumVersionMustBeLessThanMaximumVersion, MinimumVersion, MaximumVersion); + return false; + } + + return true; + } + + public override bool ProcessRecordAsync() { + //Error out for the case where multiple provider names with any version specififed + if (((!Name.IsNullOrEmpty() && Name.Length > 1) || Name[0].ContainsWildcards()) + && ((RequiredVersion != null) || (MinimumVersion != null) || (MaximumVersion != null))) + { + Error(Constants.Errors.MultipleNamesWithVersionNotAllowed); + return false; + } + + foreach (var path in Name) { + var isRooted = false; + + var resolvedPath = path; + + if (!string.IsNullOrWhiteSpace(path)) { + if (IsRooted(path)) { + if ((RequiredVersion != null) || (MaximumVersion !=null) || (MinimumVersion !=null)) { + Error(Constants.Errors.FullProviderFilePathVersionNotAllowed); + } + + try { + ProviderInfo provider = null; + Collection resolvedPaths = GetResolvedProviderPathFromPSPath(path, out provider); + + // Ensure the path is a single path from the file system provider + if ((resolvedPaths.Count > 1) || + (!String.Equals(provider.Name, "FileSystem", StringComparison.OrdinalIgnoreCase))) { + Error(Constants.Errors.FilePathMustBeFileSystemPath, path); + return false; + } + resolvedPath = resolvedPaths[0]; + + isRooted = true; + } catch (Exception ex) { + Error(Constants.Errors.FileNotFound, ex.Message); + return true; + } + } + + foreach (var p in PackageManagementService.ImportPackageProvider(this, resolvedPath, RequiredVersion.ToVersion(), + MinimumVersion.ToVersion(), MaximumVersion.ToVersion(), isRooted, Force.IsPresent)) { + WriteObject(p); + } + } + } //foreach name + + return true; + } + + public override bool EndProcessingAsync() { + return base.EndProcessingAsync(); + } + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/InstallPackage.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/InstallPackage.cs new file mode 100644 index 000000000..a4b631660 --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/InstallPackage.cs @@ -0,0 +1,260 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets { + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Management.Automation; + using Microsoft.PackageManagement.Implementation; + using Microsoft.PackageManagement.Internal.Packaging; + using Microsoft.PackageManagement.Internal.Utility.Collections; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Packaging; + + [Cmdlet(VerbsLifecycle.Install, Constants.Nouns.PackageNoun, SupportsShouldProcess = true, DefaultParameterSetName = Constants.ParameterSets.PackageBySearchSet, HelpUri = "http://go.microsoft.com/fwlink/?LinkID=517138")] + public sealed class InstallPackage : CmdletWithSearchAndSource { + + public InstallPackage() + : base(new[] { + OptionCategory.Provider, OptionCategory.Source, OptionCategory.Package, OptionCategory.Install + }) { + } + + protected override IEnumerable ParameterSets { + get { + return new[] {Constants.ParameterSets.PackageBySearchSet, Constants.ParameterSets.PackageByInputObjectSet}; + } + } + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0, ParameterSetName = Constants.ParameterSets.PackageByInputObjectSet),] + public SoftwareIdentity[] InputObject {get; set;} + + [Parameter(Position = 0, Mandatory = true, ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override string[] Name {get; set;} + + [Parameter(ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override string RequiredVersion {get; set;} + + [Parameter(ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override string MinimumVersion {get; set;} + + [Parameter(ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override string MaximumVersion {get; set;} + + [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + // Use the base Source property so relative path will be resolved + public override string[] Source { + get + { + return base.Source; + } + set + { + base.Source = value; + } + } + + + protected override void GenerateCmdletSpecificParameters(Dictionary unboundArguments) { +#if DEEP_DEBUG + Console.WriteLine("»» Entering GCSP "); +#endif + if (!IsInvocation) { +#if DEEP_DEBUG + Console.WriteLine("»»» Does not appear to be Invocation (locked:{0})", IsReentrantLocked); +#endif + var providerNames = PackageManagementService.AllProviderNames; + var whatsOnCmdline = GetDynamicParameterValue("ProviderName"); + if (whatsOnCmdline != null) { + providerNames = providerNames.Concat(whatsOnCmdline).Distinct(); + } + + DynamicParameterDictionary.AddOrSet("ProviderName", new RuntimeDefinedParameter("ProviderName", typeof(string[]), new Collection { + new ParameterAttribute { + ValueFromPipelineByPropertyName = true, + ParameterSetName = Constants.ParameterSets.PackageBySearchSet + }, + new AliasAttribute("Provider"), + new ValidateSetAttribute(providerNames.ToArray()) + })); + } + else { +#if DEEP_DEBUG + Console.WriteLine("»»» Does appear to be Invocation (locked:{0})", IsReentrantLocked); +#endif + DynamicParameterDictionary.AddOrSet("ProviderName", new RuntimeDefinedParameter("ProviderName", typeof(string[]), new Collection { + new ParameterAttribute { + ValueFromPipelineByPropertyName = true, + ParameterSetName = Constants.ParameterSets.PackageBySearchSet + }, + new AliasAttribute("Provider") + })); + } + } + + + + public override bool BeginProcessingAsync() { + return true; + } + + public override bool ProcessRecordAsync() { + if (IsPackageByObject) { + return InstallPackages(InputObject); + } + if (MyInvocation.BoundParameters.Count == 0 || (MyInvocation.BoundParameters.Count == 1 && MyInvocation.BoundParameters.ContainsKey("ProviderName")) ) { + // didn't pass in anything, (except maybe Providername) + // that's no ok -- we need some criteria + Error(Constants.Errors.MustSpecifyCriteria); + return false; + } + + if (Name.Any(each => each.ContainsWildcards())) { + Error(Constants.Errors.WildCardCharsAreNotSupported, Name.JoinWithComma()); + return false; + } + // otherwise, just do the search right now. + return base.ProcessRecordAsync(); + } + + public override bool EndProcessingAsync() { + if (IsPackageByObject) { + // we should have handled these already. + // buh-bye + return true; + } + + if (!CheckUnmatchedPackages()) { + // there are unmatched packages + // not going to install. + return false; + } + + var swids = CheckMatchedDuplicates().ReEnumerable(); + if (swids == null || !swids.Any()) + { + // there are duplicate packages + // not going to install. + return false; + } + + // good list. Let's roll... + return base.InstallPackages(swids.ToArray()); + } + + protected override void ProcessPackage(PackageProvider provider, IEnumerable searchKey, SoftwareIdentity package) { + if (WhatIf) { + // grab the dependencies and return them *first* + bool hasDependencyLoop = false; + var dependencies = GetDependenciesToInstall(package, ref hasDependencyLoop); + + if (!hasDependencyLoop) + { + foreach (var dependency in dependencies) + { + base.ProcessPackage(provider, searchKey.Select(each => each+dependency.Name).ToArray(), dependency); + } + } + } + + base.ProcessPackage(provider, searchKey, package); + } + + private IEnumerable GetDependenciesToInstall(SoftwareIdentity package, ref bool hasDependencyLoop) + { + // No dependency + if (package.Dependencies == null || package.Dependencies.Count() == 0) + { + return Enumerable.Empty(); + } + + // Returns list of dependency to be installed in the correct order that we should install them + List dependencyToBeInstalled = new List(); + + HashSet permanentlyMarked = new HashSet(new SoftwareIdentityNameVersionComparer()); + HashSet temporarilyMarked = new HashSet(new SoftwareIdentityNameVersionComparer()); + + // checks that there are no dependency loop + hasDependencyLoop = !DepthFirstVisit(package, temporarilyMarked, permanentlyMarked, dependencyToBeInstalled); + + if (!hasDependencyLoop) + { + // remove the last item of the list because that is the package itself + dependencyToBeInstalled.RemoveAt(dependencyToBeInstalled.Count - 1); + return dependencyToBeInstalled; + } + + // there are dependency loop. + return Enumerable.Empty(); + } + + /// + /// Do a dfs visit. returns false if a cycle is encountered. Add the packageItem to the list at the end of each visit + /// + /// + /// + /// + /// + /// + internal bool DepthFirstVisit(SoftwareIdentity packageItem, HashSet temporarilyMarked, + HashSet permanentlyMarked, List dependencyToBeInstalled) + { + // dependency loop detected because the element is temporarily marked + if (temporarilyMarked.Contains(packageItem)) + { + return false; + } + + // this is permanently marked. So we don't have to visit it. + // This is to resolve a case where we have: A->B->C and A->C. Then we need this when we visit C again from either B or A. + if (permanentlyMarked.Contains(packageItem)) + { + return true; + } + + // Mark this node temporarily so we can detect cycle. + temporarilyMarked.Add(packageItem); + + // Visit the dependency + foreach (var dependency in packageItem.Dependencies) + { + var dependencies = PackageManagementService.FindPackageByCanonicalId(dependency, this); + var depPkg = dependencies.OrderByDescending(pp => pp, SoftwareIdentityVersionComparer.Instance).FirstOrDefault(); + + if (!DepthFirstVisit(depPkg, temporarilyMarked, permanentlyMarked, dependencyToBeInstalled)) + { + // if dfs returns false then we have encountered a loop + return false; + } + // otherwise visit the next dependency + } + + // Add the package to the list so we can install later + dependencyToBeInstalled.Add(packageItem); + + // Done with this node so mark it permanently + permanentlyMarked.Add(packageItem); + + // Unmark it temporarily + temporarilyMarked.Remove(packageItem); + + return true; + } + + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/InstallPackageProvider.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/InstallPackageProvider.cs new file mode 100644 index 000000000..d97a134ae --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/InstallPackageProvider.cs @@ -0,0 +1,369 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics.CodeAnalysis; + using System.IO; + using System.Linq; + using System.Management.Automation; + using Microsoft.PackageManagement.Implementation; + using Microsoft.PackageManagement.Internal.Api; + using Microsoft.PackageManagement.Internal.Implementation; + using Microsoft.PackageManagement.Internal.Packaging; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Internal.Utility.Platform; + using Microsoft.PackageManagement.Internal.Utility.Plugin; + using Microsoft.PackageManagement.Packaging; + using Utility; + + [Cmdlet("Install", Constants.Nouns.PackageProviderNoun, SupportsShouldProcess = true, DefaultParameterSetName = Constants.ParameterSets.PackageBySearchSet, HelpUri = "http://go.microsoft.com/fwlink/?LinkId=626941")] + public sealed class InstallPackageProvider : CmdletWithSearchAndSource { + + private const string FilterOnTag = "FilterOnTag"; + private string _scope = "AllUsers"; + private List _sourcesFromPipeline = null; + + public InstallPackageProvider() + : base(new[] { + OptionCategory.Package, OptionCategory.Install + }) { + ShouldSelectAllProviders = true; + } + + protected override IEnumerable ParameterSets { + get { + return new[] {Constants.ParameterSets.PackageBySearchSet, Constants.ParameterSets.PackageByInputObjectSet}; + } + } + + [Parameter(Position = 0, Mandatory = true, ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override string[] Name {get; set;} + + [Parameter(ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override string RequiredVersion {get; set;} + + [Parameter(ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override string MinimumVersion {get; set;} + + [Parameter(ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override string MaximumVersion {get; set;} + + [Parameter(ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override PSCredential Credential {get; set;} + + [ValidateSetAttribute("CurrentUser", "AllUsers")] + [Parameter(ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + [Parameter(ParameterSetName = Constants.ParameterSets.PackageByInputObjectSet)] + public string Scope { + get { + return _scope; + } + set { + _scope = value; + } + } + + [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override string[] Source + { + get + { + if(InputObject != null) { + if (_sourcesFromPipeline == null) { + _sourcesFromPipeline = new List(); + + foreach (var inputObj in InputObject) { + _sourcesFromPipeline.Add(inputObj.Source); + } + } + return _sourcesFromPipeline.ToArray(); + } + return base.Source; + } + set + { + base.Source = value; + } + } + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + [Parameter(Mandatory = true, ValueFromPipeline = true, Position = 0, ParameterSetName = Constants.ParameterSets.PackageByInputObjectSet)] + public SoftwareIdentity[] InputObject {get; set;} + + protected override void GenerateCmdletSpecificParameters(Dictionary unboundArguments) { + // only generate these parameters if there is an actual call happening. it won't show in get-command -syntax + if (IsInvocation) { + //Use the InternalPackageManagementInstallOnly flag to indicate we are installing a provider right now. + //This will seperate a case from auto-bootstrapping. + var packageManagementService = PackageManagementService as PackageManagementService; + if (packageManagementService != null) { + packageManagementService.InternalPackageManagementInstallOnly = true; + } + } + } + + + protected override DynamicOption[] CachedDynamicOptions { + get { + //suppress the dynamic parameters from parent classes + return new[] { + new DynamicOption() + }; + } + } + + protected override string BootstrapNuGet + { + get + { + // Generally bootstrapping NuGet is required for the install-packageprovider as PowerShellGet uses it. + // However, when a user specifies Name as NuGet, e.g., 'install-packageprovider -name NuGet', we do not need to perform a hard + // bootstrap on NuGet because this case has been taken care of already. There is no difference from installing other packages. + return InstallingNugetProvider ? "false" : "true"; + } + } + private bool InstallingNugetProvider + { + get + { + return (Name != null) && (Name.Length == 1 && Name.ContainsAnyOfIgnoreCase("NuGet")); + } + } + + public override bool ProcessRecordAsync() { + ValidateVersion(RequiredVersion); + ValidateVersion(MinimumVersion); + ValidateVersion(MaximumVersion); + + if (!AdminPrivilege.IsElevated && Scope.EqualsIgnoreCase("AllUsers")) + { + var pkgMgmtService = PackageManagementService as PackageManagementService; + if (pkgMgmtService != null) + { + Error(Constants.Errors.InstallRequiresCurrentUserScopeParameterForNonAdminUser, pkgMgmtService.SystemAssemblyLocation, pkgMgmtService.UserAssemblyLocation); + } + return false; + } + + if (!string.IsNullOrWhiteSpace(RequiredVersion)) { + if ((!string.IsNullOrWhiteSpace(MaximumVersion)) || (!string.IsNullOrWhiteSpace(MinimumVersion))) { + Error(Constants.Errors.VersionRangeAndRequiredVersionCannotBeSpecifiedTogether); + return false; + } + } + //Error out for the case where multiple provider names with any version specififed + if (Name.IsNullOrEmpty()) { + if ((!string.IsNullOrWhiteSpace(RequiredVersion)) || (!string.IsNullOrWhiteSpace(MinimumVersion)) || (!string.IsNullOrWhiteSpace(MaximumVersion))) { + Error(Constants.Errors.MultipleNamesWithVersionNotAllowed); + return false; + } + } else { + if (Name.Any(each => each.ContainsWildcards())) { + Error(Constants.Errors.WildCardCharsAreNotSupported, Name.JoinWithComma()); + return false; + } + if ((Name.Length > 1) && + ((!string.IsNullOrWhiteSpace(RequiredVersion)) || (!string.IsNullOrWhiteSpace(MinimumVersion)) || (!string.IsNullOrWhiteSpace(MaximumVersion)))) { + Error(Constants.Errors.MultipleNamesWithVersionNotAllowed); + return false; + } + } + + if (IsPackageByObject) { + return InstallPackages(InputObject); + } + + // otherwise, just do the search right now. + return base.ProcessRecordAsync(); + } + + + + /// + /// Select existing providers on the system that will be used for searching any package providers in the repositories. + /// + protected override IEnumerable SelectedProviders { + + get { + var availableProviders = base.SelectedProviders.ToArray(); + if (availableProviders.Any(each => RequiredProviders.ContainsAnyOfIgnoreCase(each.ProviderName))) { + //For 'install-packageprovider NuGet', the PowerShellGet provider does not need to be involved. + //It will causes round trip as the PowerShellGet depends on NuGet. Here we choose the Bootstrap provider only. + return (InstallingNugetProvider)? availableProviders.Where(each => Bootstrap.EqualsIgnoreCase(each.ProviderName)): + availableProviders.Where(each => RequiredProviders.ContainsIgnoreCase(each.ProviderName)); + } + + Error(Constants.Errors.RegisterPackageSourceRequired, Source.JoinWithComma()); + return Enumerable.Empty(); + } + } + + protected override IHostApi GetProviderSpecificOption(PackageProvider pv) { + var host = this.ProviderSpecific(pv); + var host1 = host; + //add filterontag for finding providers. Provider keys are: PackageManagment and Providers + host = host.Extend( + new { + GetOptionValues = new Func>(key => { + if (key.EqualsIgnoreCase(FilterOnTag)) { + return ProviderFilters; + } + + return host1.GetOptionValues(key); + }), + }); + return host; + } + + protected override void ProcessPackage(PackageProvider provider, IEnumerable searchKey, SoftwareIdentity package) { + if (WhatIf) { + // grab the dependencies and return them *first* + + foreach (var dep in package.Dependencies) { + // note: future work may be needed if the package sources currently selected by the user don't + // contain the dependencies. + var dependendcies = PackageManagementService.FindPackageByCanonicalId(dep, this); + foreach (var depPackage in dependendcies) { + ProcessPackage(depPackage.Provider, searchKey.Select(each => each + depPackage.Name).ToArray(), depPackage); + } + } + } + base.ProcessPackage(provider, searchKey, package); + } + + protected override bool EnsurePackageIsProvider(SoftwareIdentity package) { + //Make sure the package is a provider package + return ValidatePackageProvider(package); + } + + public override bool EndProcessingAsync() + { + if (IsPackageByObject) { + // we should have handled these already. + return ImportProvider(InputObject); + } + + var unmatched = _resultsPerName.Keys.Where(each => _resultsPerName[each] == null); + + if (unmatched.Any()) { + Error(Constants.Errors.NoMatchFoundForProvider, unmatched.FirstOrDefault()); + return false; + } + + if (!CheckUnmatchedPackages()) { + // there are unmatched packages + // not going to install. + return false; + } + + var list = ProcessMatchedDuplicates().ToArray(); + + // Now install the provider + if(!base.InstallPackages(list)) { + return false; + } + + return ImportProvider(list); + } + + private bool ImportProvider(SoftwareIdentity[] list) + { + Verbose("Importing the package provider {0}", Name.JoinWithComma()); + + //after the provider gets installed, we are trying to load it + var providers = list.SelectMany(each => PackageManagementService.ImportPackageProvider(this.SuppressErrorsAndWarnings(IsProcessing), each.Name, each.Version.ToVersion(), null, null, false, true)).ToArray(); + if (providers.Any()) { + Verbose(Resources.Messages.ProviderImported, providers.Select(e => e.ProviderPath).JoinWithComma()); + return true; + } + else + { + Warning(Resources.Messages.ProviderNameDifferentFromPackageName, Name.JoinWithComma()); + } + return false; + } + + private List ProcessMatchedDuplicates() { + List filteredSoftwareIdentity = new List(); + + var packagetable = _resultsPerName.Values; + + foreach (var pkgSet in packagetable) { + + if (pkgSet.Count == 0) { + continue; + } + if (pkgSet.Count == 1) { + filteredSoftwareIdentity.AddRange(pkgSet); + continue; + } + + // the following are the case (pkgSet.Count > 1) + + // there are overmatched packages: + // are they found across multiple providers? + // are they found accross multiple sources? + // are they all from the same source? + + var providers = pkgSet.Select(each => each.ProviderName).Distinct().ToArray(); + var sources = pkgSet.Select(each => each.Source).Distinct().ToArray(); + + //Handling a case where one provider with multiple sources found the same package + if (providers.Length == 1) { + // it's matching this package multiple times in the same provider. + if (sources.Length == 1) { + string searchKey = null; + foreach (var pkg in pkgSet) { + Warning(Constants.Messages.MatchesMultiplePackages, pkg.SearchKey, pkg.ProviderName, pkg.Name, pkg.Version, pkg.Source); + searchKey = pkg.SearchKey; + } + //found the multiple matches in a single repository, we error out for asking exact -Name and -RequiredVersion. + Error(Constants.Errors.DisambiguateForInstall, searchKey, Resources.Messages.DisambiguateForInstall_SpecifyName); + + } else { + //found the multiple sources under one provider matches the same package + foreach (var subset in pkgSet) { + if (Source == null) { + //if a user does not provide -Source AND we are having matches from multiple sources, error out and asking for specifying -Source + Error(Constants.Errors.DisambiguateForInstall, subset.SearchKey, Resources.Messages.DisambiguateForInstall_SpecifySource); + break; + } + //if a user provides -Source, we try to find the match with -Source + if (Source.ContainsAnyOfIgnoreCase(subset.Source)) { + filteredSoftwareIdentity.Add(subset); + break; + } + } + } + } else { + //Handling a case where the multiple providers match one package + //In this case, both boostrap and PowerShellGet found the same package, let's take PowerShellGet as a presendence + foreach (var subset in pkgSet) { + if (subset.ProviderName.EqualsIgnoreCase(PowerShellGet)) { + //find the match + filteredSoftwareIdentity.Add(subset); + break; + } + } + } + } + + return filteredSoftwareIdentity; + } + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/Properties/AssemblyInfo.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..b2179a666 --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/Properties/AssemblyInfo.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. + +[assembly: AssemblyTitle("PackageManagement")] +[assembly: AssemblyDescription("PackageManagement PowerShell Module Implementation")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Microsoft")] +[assembly: AssemblyProduct("PackageManagement")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Normally, we wouldn't actaully permit access to the internals of the PackageManagement module +// but we're sharing code with the other our tightly-coupled Chocolatey PowerShell module + +[assembly: InternalsVisibleTo("Microsoft.PackageManagement.Test")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. + +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM + +[assembly: Guid("cef75504-d220-4860-b2df-4863f1d5c2ec")] diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/RegisterPackageSource.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/RegisterPackageSource.cs new file mode 100644 index 000000000..7f0710f23 --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/RegisterPackageSource.cs @@ -0,0 +1,207 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets { + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using System.Management.Automation; + using Microsoft.PackageManagement.Internal.Packaging; + using Microsoft.PackageManagement.Internal.Utility.Async; + using Microsoft.PackageManagement.Internal.Utility.Collections; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Utility; + using System.Security; + + [Cmdlet(VerbsLifecycle.Register, Constants.Nouns.PackageSourceNoun, SupportsShouldProcess = true, HelpUri = "http://go.microsoft.com/fwlink/?LinkID=517139")] + public sealed class RegisterPackageSource : CmdletWithProvider { + public RegisterPackageSource() + : base(new[] {OptionCategory.Provider, OptionCategory.Source}) { + } + + [Parameter] + [ValidateNotNull()] + public Uri Proxy { get; set; } + + [Parameter] + [ValidateNotNull()] + public PSCredential ProxyCredential { get; set; } + + public override string CredentialUsername + { + get + { + return Credential != null ? Credential.UserName : null; + } + } + + public override SecureString CredentialPassword + { + get + { + return Credential != null ? Credential.Password : null; + } + } + + /// + /// Returns web proxy that provider can use + /// Construct the webproxy using InternalWebProxy + /// + public override System.Net.IWebProxy WebProxy + { + get + { + if (Proxy != null) + { + return new PackageManagement.Utility.InternalWebProxy(Proxy, ProxyCredential == null ? null : ProxyCredential.GetNetworkCredential()); + } + + return null; + } + } + + protected override IEnumerable ParameterSets { + get { + return new[] {""}; + } + } + + protected override void GenerateCmdletSpecificParameters(Dictionary unboundArguments) { + if (!IsInvocation) { + var providerNames = PackageManagementService.AllProviderNames; + var whatsOnCmdline = GetDynamicParameterValue("ProviderName"); + if (whatsOnCmdline != null) { + providerNames = providerNames.Concat(whatsOnCmdline).Distinct(); + } + + DynamicParameterDictionary.AddOrSet("ProviderName", new RuntimeDefinedParameter("ProviderName", typeof(string), new Collection { + new ParameterAttribute { + ValueFromPipelineByPropertyName = true, + ParameterSetName = Constants.ParameterSets.SourceBySearchSet + }, + new AliasAttribute("Provider"), + new ValidateSetAttribute(providerNames.ToArray()) + })); + } + else { + DynamicParameterDictionary.AddOrSet("ProviderName", new RuntimeDefinedParameter("ProviderName", typeof(string), new Collection { + new ParameterAttribute { + ValueFromPipelineByPropertyName = true, + ParameterSetName = Constants.ParameterSets.SourceBySearchSet + }, + new AliasAttribute("Provider") + })); + } + } + + + [Parameter(Position = 0)] + public string Name {get; set;} + + [Parameter(Position = 1)] + public string Location {get; set;} + + [Parameter] + public PSCredential Credential {get; set;} + + [Parameter] + public SwitchParameter Trusted {get; set;} + + public override bool ProcessRecordAsync() { + if (Stopping) { + return false; + } + + var packageProvider = SelectProviders(ProviderName).ReEnumerable(); + + if (ProviderName.IsNullOrEmpty()) + { + Error(Constants.Errors.ProviderNameNotSpecified, packageProvider.Select(p => p.ProviderName).JoinWithComma()); + return false; + } + + switch (packageProvider.Count()) { + case 0: + Error(Constants.Errors.UnknownProvider, ProviderName); + return false; + + case 1: + break; + + default: + Error(Constants.Errors.MatchesMultipleProviders, packageProvider.Select(p => p.ProviderName).JoinWithComma()); + return false; + } + + + var provider = packageProvider.First(); + + using (var sources = provider.ResolvePackageSources(this).CancelWhen(CancellationEvent.Token)) { + // first, check if there is a source by this name already. + var existingSources = sources.Where(each => each.IsRegistered && each.Name.Equals(Name, StringComparison.OrdinalIgnoreCase)).ToArray(); + + if (existingSources.Any()) { + // if there is, and the user has said -Force, then let's remove it. + foreach (var existingSource in existingSources) { + if (Force) { + + if (ShouldProcess(FormatMessageString(Constants.Messages.TargetPackageSource, existingSource.Name, existingSource.Location, existingSource.ProviderName), Constants.Messages.ActionReplacePackageSource).Result) { + var removedSources = provider.RemovePackageSource(existingSource.Name, this).CancelWhen(CancellationEvent.Token); + foreach (var removedSource in removedSources) { + Verbose(Constants.Messages.OverwritingPackageSource, removedSource.Name); + } + } + } else { + Error(Constants.Errors.PackageSourceExists, existingSource.Name); + return false; + } + } + } + } + + string providerNameForProcessMessage = ProviderName.JoinWithComma(); + if (ShouldProcess(FormatMessageString(Constants.Messages.TargetPackageSource, Name, Location, providerNameForProcessMessage), FormatMessageString(Constants.Messages.ActionRegisterPackageSource)).Result) + { + //Need to resolve the path created via psdrive. + //e.g., New-PSDrive -Name x -PSProvider FileSystem -Root \\foobar\myfolder. Here we are resolving x:\ + try + { + if (FilesystemExtensions.LooksLikeAFilename(Location)) + { + ProviderInfo providerInfo = null; + var resolvedPaths = GetResolvedProviderPathFromPSPath(Location, out providerInfo); + + // Ensure the path is a single path from the file system provider + if ((providerInfo != null) && (resolvedPaths.Count == 1) && String.Equals(providerInfo.Name, "FileSystem", StringComparison.OrdinalIgnoreCase)) + { + Location = resolvedPaths[0]; + } + } + } catch (Exception) { + //allow to continue handling the cases other than file system + } + + using (var added = provider.AddPackageSource(Name, Location, Trusted, this).CancelWhen(CancellationEvent.Token)) { + foreach (var addedSource in added) { + WriteObject(addedSource); + } + } + return true; + } + + return false; + } + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/SavePackage.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/SavePackage.cs new file mode 100644 index 000000000..1d341eed4 --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/SavePackage.cs @@ -0,0 +1,279 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets { + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.IO; + using System.Linq; + using System.Management.Automation; + using Microsoft.PackageManagement.Implementation; + using Microsoft.PackageManagement.Internal.Implementation; + using Microsoft.PackageManagement.Internal.Packaging; + using Microsoft.PackageManagement.Internal.Utility.Async; + using Microsoft.PackageManagement.Internal.Utility.Collections; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Packaging; + using Utility; + using Directory = System.IO.Directory; + using File = System.IO.File; + using Microsoft.PackageManagement.Internal.Utility.Versions; + + [Cmdlet(VerbsData.Save, Constants.Nouns.PackageNoun, SupportsShouldProcess = true, HelpUri = "http://go.microsoft.com/fwlink/?LinkID=517140")] + public sealed class SavePackage : CmdletWithSearchAndSource { + public SavePackage() + : base(new[] { + OptionCategory.Provider, OptionCategory.Source, OptionCategory.Package + }) { + } + + protected override IEnumerable ParameterSets { + get { + return new[] {Constants.ParameterSets.PackageByInputObjectSet, ""}; + } + } + + [Parameter(Position = 0, Mandatory = true, ValueFromPipelineByPropertyName = true, ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override string[] Name { get; set; } + + [Parameter(ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override string RequiredVersion { get; set; } + + [Parameter(ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override string MinimumVersion { get; set; } + + [Parameter(ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override string MaximumVersion { get; set; } + + [Parameter(ValueFromPipelineByPropertyName = true, ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + // Use the base Source property so relative path will be resolved + public override string[] Source + { + get + { + return base.Source; + } + set + { + base.Source = value; + } + } + + protected override void GenerateCmdletSpecificParameters(Dictionary unboundArguments) { + if (!IsInvocation) { + var providerNames = PackageManagementService.AllProviderNames; + var whatsOnCmdline = GetDynamicParameterValue("ProviderName"); + if (whatsOnCmdline != null) { + providerNames = providerNames.Concat(whatsOnCmdline).Distinct(); + } + + DynamicParameterDictionary.AddOrSet("ProviderName", new RuntimeDefinedParameter("ProviderName", typeof(string[]), new Collection { + new ParameterAttribute { + ValueFromPipelineByPropertyName = true, + ParameterSetName = Constants.ParameterSets.PackageBySearchSet + }, + new AliasAttribute("Provider"), + new ValidateSetAttribute(providerNames.ToArray()) + })); + } + else { + DynamicParameterDictionary.AddOrSet("ProviderName", new RuntimeDefinedParameter("ProviderName", typeof(string[]), new Collection { + new ParameterAttribute { + ValueFromPipelineByPropertyName = true, + ParameterSetName = Constants.ParameterSets.PackageBySearchSet + }, + new AliasAttribute("Provider") + })); + } + } + + [Parameter] + public string Path {get; set;} + + [Parameter] + public string LiteralPath {get; set;} + + [Parameter(Mandatory = true, ValueFromPipeline = true, ParameterSetName = Constants.ParameterSets.PackageByInputObjectSet)] + public SoftwareIdentity InputObject {get; set;} + + private string SaveFileName(string packageName) { + string resolvedPath = null; + + try + { + if (!string.IsNullOrWhiteSpace(Path)) + { + resolvedPath = ResolveExistingFolderPath(Path, !Force); + } + + if (!string.IsNullOrWhiteSpace(LiteralPath)) + { + // Validate that the path exists + try + { + SessionState.InvokeProvider.Item.Get(new string[] { LiteralPath }, false, true); + } + catch (ItemNotFoundException) + { + if(!Force) + { + throw; + } + } + + resolvedPath = LiteralPath; + } + + if (string.IsNullOrWhiteSpace(resolvedPath)) { + Error(Constants.Errors.DestinationPathInvalid, resolvedPath, packageName); + return null; + } + + // If the destination directory doesn't exist, create it + if (!Directory.Exists(resolvedPath)) { + Directory.CreateDirectory(resolvedPath); + } + + // don't append path and package name here + return resolvedPath; + } + catch (Exception e) + { + Error(Constants.Errors.SavePackageError, e.Message); + return null; + } + } + + public override bool ProcessRecordAsync() { + if (string.IsNullOrWhiteSpace(Path) && string.IsNullOrWhiteSpace(LiteralPath)) { + Error(Constants.Errors.DestinationOrLiteralPathNotSpecified); + return false; + } + + if (IsPackageByObject) + { + return DownloadPackage(InputObject); + } + + if (Name.Any(each => each.ContainsWildcards())) + { + Error(Constants.Errors.WildCardCharsAreNotSupported, Name.JoinWithComma()); + return false; + } + + return base.ProcessRecordAsync(); + } + + public override bool EndProcessingAsync() + { + if (IsCanceled) + { + return false; + } + + if (IsPackageByObject) + { + // we should have handled these already in ProcessRecordAsync() + return true; + } + + if (!CheckUnmatchedPackages()) + { + // there are unmatched packages + // not going to install. + return false; + } + + var swids = CheckMatchedDuplicates().ReEnumerable(); + if (swids == null || !swids.Any()) + { + // there are duplicate packages + // not going to install. + return false; + } + + // good list. Let's roll... + return DownloadPackage(swids.ToArray()); + } + + private bool DownloadPackage(params SoftwareIdentity[] packagesToSave) + { + foreach (var package in packagesToSave) + { + if (IsCanceled) + { + return false; + } + + var provider = package.Provider; + + + if (!provider.IsMethodImplemented("DownloadPackage")) + { + Error(Constants.Errors.MethodNotImplemented, provider.ProviderName, "Save-Package"); + return false; + } + + // if we do save-package jquery -path C:\test then savepath would be C:\test + var savePath = SaveFileName(package.PackageFilename); + + bool mainPackageDownloaded = false; + + if (!string.IsNullOrWhiteSpace(savePath)) + { + // let the provider handles everything + // message would be something like What if: Performing the operation "Save Package" on target "'jQuery' to location 'C:\test\test'". + if (ShouldProcess(FormatMessageString(Resources.Messages.SavePackageWhatIfDescription, package.Name, savePath), FormatMessageString(Resources.Messages.SavePackage)).Result) + { + var host = this.ProviderSpecific(provider); + foreach (var downloadedPkg in provider.DownloadPackage(package, savePath, host).CancelWhen(CancellationEvent.Token)) + //foreach (var downloadedPkg in provider.DownloadPackage(package, savePath, ErrorAndWarningContinue ? host.SuppressErrorsAndWarnings(IsProcessing) : host).CancelWhen(CancellationEvent.Token)) + { + if (IsCanceled) + { + Error(Constants.Errors.ProviderFailToDownloadFile, downloadedPkg.PackageFilename, provider.ProviderName); + return false; + } + + // check whether main package is downloaded; + if (downloadedPkg.Name.EqualsIgnoreCase(package.Name) && (FourPartVersion)downloadedPkg.Version >= (FourPartVersion)package.Version) + { + mainPackageDownloaded = true; + } + + WriteObject(AddPropertyToSoftwareIdentity(downloadedPkg)); + LogEvent(EventTask.Download, EventId.Save, Resources.Messages.PackageSaved, downloadedPkg.Name, downloadedPkg.Version, downloadedPkg.ProviderName, downloadedPkg.Source ?? string.Empty, downloadedPkg.Status ?? string.Empty, downloadedPkg.InstallationPath ?? string.Empty); + TraceMessage(Constants.SavePackageTrace, downloadedPkg); + } + + } + else + { + // What if scenario, don't error out + return true; + } + } + + if (!mainPackageDownloaded) + { + Error(Constants.Errors.ProviderFailToDownloadFile, package.PackageFilename, provider.ProviderName); + return false; + } + } + + return true; + } + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/SetPackageSource.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/SetPackageSource.cs new file mode 100644 index 000000000..6431ba764 --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/SetPackageSource.cs @@ -0,0 +1,263 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets { + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using System.Management.Automation; + using Microsoft.PackageManagement.Internal.Api; + using Microsoft.PackageManagement.Internal.Packaging; + using Microsoft.PackageManagement.Internal.Utility.Async; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Internal.Utility.Plugin; + using Microsoft.PackageManagement.Packaging; + using Utility; + using System.Security; + + [Cmdlet(VerbsCommon.Set, Constants.Nouns.PackageSourceNoun, SupportsShouldProcess = true, DefaultParameterSetName = Constants.ParameterSets.SourceBySearchSet, HelpUri = "http://go.microsoft.com/fwlink/?LinkID=517141")] + public sealed class SetPackageSource : CmdletWithProvider { + [Parameter(ValueFromPipeline = true, ParameterSetName = Constants.ParameterSets.SourceByInputObjectSet, Mandatory = true)] + public PackageSource InputObject; + + public SetPackageSource() : base(new[] {OptionCategory.Provider, OptionCategory.Source}) { + } + + [Parameter] + [ValidateNotNull()] + public Uri Proxy { get; set; } + + [Parameter] + [ValidateNotNull()] + public PSCredential ProxyCredential { get; set; } + + /// + /// Returns web proxy that provider can use + /// Construct the webproxy using InternalWebProxy + /// + public override System.Net.IWebProxy WebProxy + { + get + { + if (Proxy != null) + { + return new PackageManagement.Utility.InternalWebProxy(Proxy, ProxyCredential == null ? null : ProxyCredential.GetNetworkCredential()); + } + + return null; + } + } + + [Parameter] + public PSCredential Credential { get; set; } + + public override string CredentialUsername + { + get + { + return Credential != null ? Credential.UserName : null; + } + } + + public override SecureString CredentialPassword + { + get + { + return Credential != null ? Credential.Password : null; + } + } + + protected override IEnumerable ParameterSets { + get { + return new[] {Constants.ParameterSets.SourceByInputObjectSet, Constants.ParameterSets.SourceBySearchSet}; + } + } + + protected override void GenerateCmdletSpecificParameters(Dictionary unboundArguments) { + if (!IsInvocation) { + var providerNames = PackageManagementService.AllProviderNames; + var whatsOnCmdline = GetDynamicParameterValue("ProviderName"); + if (whatsOnCmdline != null) { + providerNames = providerNames.Concat(whatsOnCmdline).Distinct(); + } + + DynamicParameterDictionary.AddOrSet("ProviderName", new RuntimeDefinedParameter("ProviderName", typeof(string), new Collection { + new ParameterAttribute { + ValueFromPipelineByPropertyName = true, + ParameterSetName = Constants.ParameterSets.SourceBySearchSet, + }, + new AliasAttribute("Provider"), + new ValidateSetAttribute(providerNames.ToArray()) + })); + } + else { + DynamicParameterDictionary.AddOrSet("ProviderName", new RuntimeDefinedParameter("ProviderName", typeof(string), new Collection { + new ParameterAttribute { + ValueFromPipelineByPropertyName = true, + ParameterSetName = Constants.ParameterSets.SourceBySearchSet, + }, + new AliasAttribute("Provider") + })); + } + } + + [Alias("SourceName")] + [Parameter(Position = 0, ParameterSetName = Constants.ParameterSets.SourceBySearchSet)] + public string Name {get; set;} + + [Parameter(ParameterSetName = Constants.ParameterSets.SourceBySearchSet)] + public string Location {get; set;} + + [Parameter] + public string NewLocation {get; set;} + + [Parameter] + public string NewName {get; set;} + + [Parameter] + public SwitchParameter Trusted {get; set;} + + public override IEnumerable Sources { + get { + if (string.IsNullOrWhiteSpace(Name) && string.IsNullOrWhiteSpace(Location)) { + return Microsoft.PackageManagement.Internal.Constants.Empty; + } + + return new[] { + Name ?? Location + }; + } + } + + /// + /// This can be used when we want to override some of the functions that are passed + /// in as the implementation of the IHostApi (ie, 'request object'). + /// Because the DynamicInterface DuckTyper will use all the objects passed in in order + /// to implement a given API, if we put in delegates to handle some of the functions + /// they will get called instead of the implementation in the current class. ('this') + /// + private IHostApi UpdatePackageSourceRequest { + get { + return new object[] { + new { + // override the GetOptionKeys and the GetOptionValues on the fly. + GetOptionKeys = new Func>(() => OptionKeys.ConcatSingleItem("IsUpdatePackageSource")), + + GetOptionValues = new Func>((key) => { + if (key != null && key.EqualsIgnoreCase("IsUpdatePackageSource")) { + return "true".SingleItemAsEnumerable(); + } + return GetOptionValues(key); + }) + }, + this, + }.As(); + } + } + + private void UpdatePackageSource(PackageSource source) { + if (WhatIf) { + + var p = new PSObject(source); + + if (!string.IsNullOrWhiteSpace(NewName)) { + p.Properties.Remove("Name"); + p.Properties.Add( new PSNoteProperty("Name",NewName)); + } + + if (!string.IsNullOrWhiteSpace(NewLocation)) { + p.Properties.Remove("Location"); + p.Properties.Add(new PSNoteProperty("Location", NewLocation)); + } + + if (Trusted.IsPresent) { + p.Properties.Remove("Trusted"); + p.Properties.Add(new PSNoteProperty("Trusted", Trusted.ToBool())); + } + + WriteObject(p); + return; + } + if (string.IsNullOrWhiteSpace(NewName)) { + // this is a replacement of an existing package source, we're *not* changing the name. (easy) + + foreach (var src in source.Provider.AddPackageSource(string.IsNullOrWhiteSpace(NewName) ? source.Name : NewName, string.IsNullOrWhiteSpace(NewLocation) ? source.Location : NewLocation, Trusted, UpdatePackageSourceRequest)) { + WriteObject(src); + } + + } else { + // we're renaming a source. + // a bit more messy at this point + // create a new package source first + + bool removed = false; + + foreach (var src in source.Provider.AddPackageSource(NewName, string.IsNullOrWhiteSpace(NewLocation) ? source.Location : NewLocation, Trusted.IsPresent ? Trusted.ToBool() : source.IsTrusted, this)) { + WriteObject(src); + if (!removed) + { + // if we are able to successfully add a source, then we remove the original source that was supposed to be replace. + // This will only happen once (as there is only one original source) + source.Provider.RemovePackageSource(source.Name, this); + removed = true; + } + } + } + } + + public override bool ProcessRecordAsync() { + if (IsSourceByObject) { + // we've already got the package source + UpdatePackageSource(InputObject); + return true; + } + + if (string.IsNullOrWhiteSpace(Name) && string.IsNullOrWhiteSpace(Location)) { + Error(Constants.Errors.NameOrLocationRequired); + return false; + } + + // otherwise, we're just changing a source by name + var prov = SelectedProviders.ToArray(); + + if (Stopping) { + return false; + } + + if (prov.Length == 0) { + if (ProviderName.IsNullOrEmpty() || string.IsNullOrWhiteSpace(ProviderName[0])) { + return Error(Constants.Errors.UnableToFindProviderForSource, Name ?? Location); + } + return Error(Constants.Errors.UnknownProvider, ProviderName[0]); + } + + if (prov.Length > 0) { + var sources = prov.SelectMany(each => each.ResolvePackageSources(this.SuppressErrorsAndWarnings(IsProcessing)).Where(source => source.IsRegistered && + (Name == null || source.Name.EqualsIgnoreCase(Name)) || (Location == null || source.Location.EqualsIgnoreCase(Location))).ToArray()).ToArray(); + + if (sources.Length == 0) { + return Error(Constants.Errors.SourceNotFound, Name); + } + + if (sources.Length > 1) { + return Error(Constants.Errors.SourceFoundInMultipleProviders, Name, prov.Select(each => each.ProviderName).JoinWithComma()); + } + + UpdatePackageSource(sources[0]); + } + return true; + } + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/UninstallPackage.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/UninstallPackage.cs new file mode 100644 index 000000000..7a99eb647 --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/UninstallPackage.cs @@ -0,0 +1,188 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets { + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Management.Automation; + using Microsoft.PackageManagement.Internal.Packaging; + using Microsoft.PackageManagement.Internal.Utility.Async; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Packaging; + using Utility; + + [Cmdlet(VerbsLifecycle.Uninstall, Constants.Nouns.PackageNoun, SupportsShouldProcess = true, HelpUri = "http://go.microsoft.com/fwlink/?LinkID=517142")] + public sealed class UninstallPackage : GetPackage { + private Dictionary> _resultsPerName = new Dictionary>(); + + protected override IEnumerable ParameterSets { + get { + return new[] {Constants.ParameterSets.PackageByInputObjectSet, Constants.ParameterSets.PackageBySearchSet}; + } + } + + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ParameterSetName = Constants.ParameterSets.PackageByInputObjectSet)] + public SoftwareIdentity[] InputObject {get; set;} + + [Parameter(Mandatory = true, Position = 0, ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override string[] Name {get; set;} + + [Parameter(ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override string RequiredVersion {get; set;} + + [Parameter(ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override string MinimumVersion {get; set;} + + [Parameter(ParameterSetName = Constants.ParameterSets.PackageBySearchSet)] + public override string MaximumVersion {get; set;} + + protected override void GenerateCmdletSpecificParameters(Dictionary unboundArguments) { + if (!IsInvocation) { + var providerNames = PackageManagementService.AllProviderNames; + var whatsOnCmdline = GetDynamicParameterValue("ProviderName"); + if (whatsOnCmdline != null) { + providerNames = providerNames.Concat(whatsOnCmdline).Distinct(); + } + + DynamicParameterDictionary.AddOrSet("ProviderName", new RuntimeDefinedParameter("ProviderName", typeof(string[]), new Collection { + new ParameterAttribute { + ValueFromPipelineByPropertyName = true, + ParameterSetName = Constants.ParameterSets.PackageBySearchSet + }, + new AliasAttribute("Provider"), + new ValidateSetAttribute(providerNames.ToArray()) + })); + } + else { + DynamicParameterDictionary.AddOrSet("ProviderName", new RuntimeDefinedParameter("ProviderName", typeof(string[]), new Collection { + new ParameterAttribute { + ValueFromPipelineByPropertyName = true, + ParameterSetName = Constants.ParameterSets.PackageBySearchSet + }, + new AliasAttribute("Provider") + })); + } + } + protected override string BootstrapNuGet + { + get + { + // need bootstrap NuGet if does not exists. + return "true"; + } + } + public override bool ProcessRecordAsync() { + if (IsPackageByObject) { + return UninstallPackages(InputObject); + } + + if (Name.Any(each => each.ContainsWildcards())) { + Error(Constants.Errors.WildCardCharsAreNotSupported, Name.JoinWithComma()); + return false; + } + + if (Name.Any(each => string.IsNullOrWhiteSpace(each))) + { + Error(Constants.Errors.WhitespacesAreNotSupported, Name.JoinWithComma()); + return false; + } + + // otherwise do the same as get-package and collect the results for the EndProcessingAsync + base.ProcessRecordAsync(); + + return true; + } + + protected override void ProcessPackage(SoftwareIdentity package) { + if (null != package && !package.CanonicalId.IsNullOrEmpty()) + { + // mark down that we found something for that query + // Need a identifier for maintaining the list of packages to uninstall in a dictionary. + // Canonical ID consists of packagename, version, repo which is unique + _resultsPerName.GetOrAdd(package.CanonicalId, () => new List()).Add(package); + } + } + + public override bool EndProcessingAsync() { + // Show errors before? + foreach (var name in UnprocessedNames) { + Error(Constants.Errors.NoMatchFound, name); + } + + if (_resultsPerName == null) { + return true; + } + + // if we encountered an error, we're not going to even do anything. + if (Stopping) { + return false; + } + + foreach (var n in _resultsPerName.Keys) { + // check if we have a 1 package per name + if (_resultsPerName[n].Count > 1) { + Error(Constants.Errors.DisambiguateForUninstall, n, _resultsPerName[n]); + return false; + } + + if (Stopping) { + return false; + } + + if (!UninstallPackages(_resultsPerName[n])) { + return false; + } + } + return true; + } + + private bool UninstallPackages(IEnumerable packagesToUnInstall) { + foreach (var pkg in packagesToUnInstall) { + var provider = SelectProviders(pkg.ProviderName).FirstOrDefault(); + Debug("Uninstalling package {0} with provider {1}", pkg.Name, provider.ProviderName); + + if (provider == null) { + Error(Constants.Errors.UnknownProvider, pkg.ProviderName); + return false; + } + + try { + if (ShouldProcessPackageUninstall(pkg.Name, pkg.Version)) { + foreach (var installedPkg in provider.UninstallPackage(pkg, this).CancelWhen(CancellationEvent.Token)) { + if (IsCanceled) { + return false; + } + WriteObject(installedPkg); + LogEvent(EventTask.Uninstall, EventId.Uninstall, Resources.Messages.PackageUnInstalled, installedPkg.Name, installedPkg.Version, installedPkg.ProviderName, installedPkg.Source ?? string.Empty, installedPkg.Status ?? string.Empty, installedPkg.InstallationPath ?? string.Empty); + TraceMessage(Constants.UnInstallPackageTrace, installedPkg); + } + } + } catch (Exception e) { + e.Dump(); + Error(Constants.Errors.UninstallationFailure, pkg.Name); + return false; + } + } + return true; + } + + public bool ShouldProcessPackageUninstall(string packageName, string version) { + return Force || ShouldProcess(FormatMessageString(Constants.Messages.TargetPackageVersion, packageName, version), FormatMessageString(Constants.Messages.ActionUninstallPackage)).Result; + } + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Cmdlets/UnregisterPackageSource.cs b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/UnregisterPackageSource.cs new file mode 100644 index 000000000..93e1f49bb --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Cmdlets/UnregisterPackageSource.cs @@ -0,0 +1,153 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Cmdlets { + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using System.Management.Automation; + using Microsoft.PackageManagement.Internal.Packaging; + using Microsoft.PackageManagement.Internal.Utility.Async; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Packaging; + using Utility; + + [Cmdlet(VerbsLifecycle.Unregister, Constants.Nouns.PackageSourceNoun, SupportsShouldProcess = true, HelpUri = "http://go.microsoft.com/fwlink/?LinkID=517143")] + public sealed class UnregisterPackageSource : CmdletWithSource { + public UnregisterPackageSource() + : base(new[] {OptionCategory.Provider, OptionCategory.Source}) { + } + + protected override IEnumerable ParameterSets { + get { + return new[] {Constants.ParameterSets.SourceByInputObjectSet, Constants.ParameterSets.SourceBySearchSet}; + } + } + + + protected override void GenerateCmdletSpecificParameters(Dictionary unboundArguments) { + if (!IsInvocation) { + var providerNames = PackageManagementService.AllProviderNames; + var whatsOnCmdline = GetDynamicParameterValue("ProviderName"); + if (whatsOnCmdline != null) { + providerNames = providerNames.Concat(whatsOnCmdline).Distinct(); + } + + DynamicParameterDictionary.AddOrSet("ProviderName", new RuntimeDefinedParameter("ProviderName", typeof(string), new Collection { + new ParameterAttribute { + ValueFromPipelineByPropertyName = true, + ParameterSetName = Constants.ParameterSets.SourceBySearchSet + }, + new AliasAttribute("Provider"), + new ValidateSetAttribute(providerNames.ToArray()) + })); + } + else { + DynamicParameterDictionary.AddOrSet("ProviderName", new RuntimeDefinedParameter("ProviderName", typeof(string), new Collection { + new ParameterAttribute { + ValueFromPipelineByPropertyName = true, + ParameterSetName = Constants.ParameterSets.SourceBySearchSet + }, + new AliasAttribute("Provider") + })); + } + } + + [Alias("Name")] + [Parameter(Position = 0, ParameterSetName = Constants.ParameterSets.SourceBySearchSet)] + public string Source {get; set;} + + [Parameter(ParameterSetName = Constants.ParameterSets.SourceBySearchSet)] + public string Location {get; set;} + + public override IEnumerable Sources { + get { + if (string.IsNullOrWhiteSpace(Source)) { + return new string[0]; + } + return new[] { + Source + }; + } + } + + public override bool ProcessRecordAsync() { + if (IsSourceByObject) { + foreach (var source in InputObject) { + if (Stopping) { + return false; + } + + var provider = SelectProviders(source.ProviderName).FirstOrDefault(); + if (provider == null) { + if (string.IsNullOrWhiteSpace(source.ProviderName)) { + return Error(Constants.Errors.UnableToFindProviderForSource, source.Name); + } + return Error(Constants.Errors.UnknownProvider, source.ProviderName); + } + Unregister(source); + } + return true; + } + + + if (string.IsNullOrWhiteSpace(Source) && string.IsNullOrWhiteSpace(Location)) { + Error(Constants.Errors.NameOrLocationRequired); + return false; + } + + // otherwise, we're just deleting a source by name + var prov = SelectedProviders.ToArray(); + + if (Stopping) { + return false; + } + + if (prov.Length == 0) { + if (ProviderName.IsNullOrEmpty() || string.IsNullOrWhiteSpace(ProviderName[0])) { + return Error(Constants.Errors.UnableToFindProviderForSource, Source ?? Location); + } + return Error(Constants.Errors.UnknownProvider, ProviderName[0]); + } + + if (prov.Length > 0) { + var sources = prov.SelectMany(each => each.ResolvePackageSources(this.SuppressErrorsAndWarnings(IsProcessing)).Where(source => source.IsRegistered && (source.Name.EqualsIgnoreCase(Source) || source.Location.EqualsIgnoreCase(Source) || source.Location.EqualsIgnoreCase(Location))).ToArray()).ToArray(); + + if (sources.Length == 0) { + return Error(Constants.Errors.SourceNotFound, Source ?? Location); + } + + if (sources.Length > 1) { + return Error(Constants.Errors.SourceFoundInMultipleProviders, Source ?? Location, prov.Select(each => each.ProviderName).JoinWithComma()); + } + + return Unregister(sources[0]); + } + + return true; + } + + public bool Unregister(PackageSource source) { + if (source == null) { + throw new ArgumentNullException("source"); + } + if (ShouldProcess(FormatMessageString(Constants.Messages.TargetPackageSource, source.Name, source.Location, source.ProviderName), FormatMessageString(Constants.Messages.ActionUnregisterPackageSource)).Result) { + source.Provider.RemovePackageSource(source.Name, this).Wait(); + return true; + } + return false; + } + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Constants.cs b/src/Microsoft.PowerShell.PackageManagement/Constants.cs new file mode 100644 index 000000000..f74d98f6e --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Constants.cs @@ -0,0 +1,229 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement { + using System.Management.Automation; + using Utility; + + internal static class Constants { + internal const int DefaultTimeout = 60*60; // 60 minutes + // todo: setting responsiveness to 15 minutes until we're sure + // that it works right + internal const int DefaultResponsiveness = 15 * 60 ; // 30 seconds + // internal const int DefaultResponsiveness = 30; // 30 seconds + + // cmdlet naming/etc + + internal class ParameterSets { + + // To avoid the FxCop complains: CA1812: Avoid uninstantiated internal + private ParameterSets() { + } + + internal const string PackageBySearchSet = "PackageBySearch"; + internal const string PackageByInputObjectSet = "PackageByInputObject"; + internal const string SourceByInputObjectSet = "SourceByInputObject"; + internal const string SourceBySearchSet = "SourceBySearch"; + + internal const string DestinationPathSet = "DestinationPath"; + internal const string LiteralPathSet = "LiteralPath"; + } + + internal const string AssemblyProviderType = "assembly"; + internal static object[] NoParameters = new object[0]; + internal const string InstallPackageTrace = "Microsoft.Windows.PowerShell.PackageManagement_InstallPackage"; + internal const string UnInstallPackageTrace = "Microsoft.Windows.PowerShell.PackageManagement_UnInstallPackage"; + internal const string SavePackageTrace = "Microsoft.Windows.PowerShell.PackageManagement_InstallPackage"; + + internal const string PowerShellProviderName = "Microsoft-Windows-PowerShell"; + internal const string NewWinEvent = @"Microsoft.PowerShell.Diagnostics\New-WinEvent"; + + // messages + + // Implementation Note: + // Because PackageManagement allows the application layer closest to the user (host) to be in ultimate + // control of spcifying messages to the end user, and falls back up the chain of responsibility + // when resolving Messages from resources, we have prefixed the constants with MSG: in order + // to *know* when we're trying to resolve a message. + + // As an optimization step, if the MSG: isn't present, then the application layer need not bother + // resolving the message (nor asking up the chain) since it's not a message id, but rather an + // already resolved string. + + internal static class Errors { + // ReSharper disable InconsistentNaming + public static ErrorMessage DestinationOrLiteralPathNotSpecified = new ErrorMessage(Messages.DestinationOrLiteralPathRequired, ErrorCategory.InvalidArgument); + public static ErrorMessage DestinationPathInvalid = new ErrorMessage(Messages.DestinationPathInvalid, ErrorCategory.InvalidArgument); + public static ErrorMessage DisambiguateForInstall = new ErrorMessage(Messages.DisambiguateForInstall, ErrorCategory.InvalidArgument); + public static ErrorMessage DisambiguateForUninstall = new ErrorMessage(Messages.DisambiguateForUninstall, ErrorCategory.InvalidArgument); + public static ErrorMessage InstallationFailure = new ErrorMessage(Messages.InstallationFailure, ErrorCategory.InvalidOperation); + public static ErrorMessage MatchesMultipleProviders = new ErrorMessage(Messages.MatchesMultipleProviders, ErrorCategory.InvalidArgument); + public static ErrorMessage MustSpecifyCriteria = new ErrorMessage(Messages.MustSpecifyCriteria, ErrorCategory.InvalidArgument); + public static ErrorMessage NameOrLocationRequired = new ErrorMessage(Messages.NameOrLocationRequired, ErrorCategory.InvalidArgument); + public static ErrorMessage NoMatchForProvidersAndSources = new ErrorMessage(Messages.NoMatchForProvidersAndSources, ErrorCategory.InvalidArgument); + public static ErrorMessage NoMatchFound = new ErrorMessage(Messages.NoMatchFound, ErrorCategory.ObjectNotFound); + public static ErrorMessage NoMatchFoundForCriteria = new ErrorMessage(Messages.NoMatchFoundForCriteria, ErrorCategory.ObjectNotFound); + public static ErrorMessage NoPackagesFoundForProvider = new ErrorMessage(Messages.NoPackagesFoundForProvider, ErrorCategory.ObjectNotFound); + public static ErrorMessage PackageInstallRequiresOption = new ErrorMessage(Messages.PackageInstallRequiresOption, ErrorCategory.InvalidArgument); + public static ErrorMessage PackageSourceExists = new ErrorMessage(Messages.PackageSourceExists, ErrorCategory.ResourceExists); + public static ErrorMessage SourceFoundInMultipleProviders = new ErrorMessage(Messages.SourceFoundInMultipleProviders, ErrorCategory.InvalidArgument); + public static ErrorMessage SourceNotFound = new ErrorMessage(Messages.SourceNotFound, ErrorCategory.ObjectNotFound); + public static ErrorMessage UnableToFindProviderForSource = new ErrorMessage(Messages.UnableToFindProviderForSource, ErrorCategory.ObjectNotFound); + public static ErrorMessage UninstallationFailure = new ErrorMessage(Messages.UninstallationFailure, ErrorCategory.InvalidOperation); + public static ErrorMessage UnknownProvider = new ErrorMessage(Microsoft.PackageManagement.Internal.Constants.Messages.UnknownProvider, ErrorCategory.ObjectNotFound); + public static ErrorMessage UnknownProviderFromActivatedList = new ErrorMessage(Messages.UnknownProviderFromActivatedList, ErrorCategory.ObjectNotFound); + public static ErrorMessage UnknownProviders = new ErrorMessage(Messages.UnknownProviders, ErrorCategory.InvalidArgument); + public static ErrorMessage PackageFileExists = new ErrorMessage(Messages.PackageFileExists, ErrorCategory.InvalidArgument); + public static ErrorMessage ProviderFailToDownloadFile = new ErrorMessage(Messages.ProviderFailToDownloadFile, ErrorCategory.InvalidOperation); + public static ErrorMessage UnableToOverwrite = new ErrorMessage(Messages.UnableToOverwrite, ErrorCategory.InvalidResult); + public static ErrorMessage FilePathMustBeFileSystemPath = new ErrorMessage(Messages.FilePathMustBeFileSystemPath, ErrorCategory.ObjectNotFound); + public static ErrorMessage SavePackageError = new ErrorMessage(Messages.SavePackageError, ErrorCategory.InvalidArgument); + public static ErrorMessage UnableToFindDependencyPackage = new ErrorMessage(Messages.UnableToFindDependencyPackage, ErrorCategory.ObjectNotFound); + public static ErrorMessage ProviderNameNotSpecified = new ErrorMessage(Messages.ProviderNameNotSpecified, ErrorCategory.NotSpecified); + public static ErrorMessage SpecifiedProviderMissingRequiredOption = new ErrorMessage(Messages.SpecifiedProviderMissingRequiredOption, ErrorCategory.InvalidArgument); + public static ErrorMessage FileNotFound = new ErrorMessage(Messages.FileNotFound, ErrorCategory.ObjectNotFound); + public static ErrorMessage FolderNotFound = new ErrorMessage(Messages.FolderNotFound, ErrorCategory.ObjectNotFound); + public static ErrorMessage UnhandledException = new ErrorMessage(Messages.UnhandledException, ErrorCategory.InvalidOperation); + public static ErrorMessage MoreThanOneFolderMatched = new ErrorMessage(Messages.MoreThanOneFolderMatched, ErrorCategory.ObjectNotFound); + public static ErrorMessage MoreThanOneFileMatched = new ErrorMessage(Messages.MoreThanOneFileMatched, ErrorCategory.ObjectNotFound); + public static ErrorMessage VersionRangeAndRequiredVersionCannotBeSpecifiedTogether = new ErrorMessage(Messages.VersionRangeAndRequiredVersionCannotBeSpecifiedTogether, ErrorCategory.InvalidArgument); + public static ErrorMessage AllVersionsCannotBeUsedWithOtherVersionParameters = new ErrorMessage(Messages.AllVersionsCannotBeUsedWithOtherVersionParameters, ErrorCategory.InvalidArgument); + public static ErrorMessage InvalidVersion = new ErrorMessage(Messages.InvalidVersion, ErrorCategory.InvalidArgument); + public static ErrorMessage MultipleNamesWithVersionNotAllowed = new ErrorMessage(Messages.MultipleNamesWithVersionNotAllowed, ErrorCategory.InvalidArgument); + public static ErrorMessage WildCardCharsAreNotSupported = new ErrorMessage(Messages.WildCardCharsAreNotSupported, ErrorCategory.InvalidArgument); + public static ErrorMessage WhitespacesAreNotSupported = new ErrorMessage(Messages.WhitespacesAreNotSupported, ErrorCategory.InvalidArgument); + public static ErrorMessage MinimumVersionMustBeLessThanMaximumVersion = new ErrorMessage(Messages.MinimumVersionMustBeLessThanMaximumVersion, ErrorCategory.InvalidArgument); + public static ErrorMessage FullProviderFilePathVersionNotAllowed = new ErrorMessage(Messages.FullProviderFilePathVersionNotAllowed, ErrorCategory.InvalidArgument); + public static ErrorMessage RegisterPackageSourceRequired = new ErrorMessage(Messages.RegisterPackageSourceRequired, ErrorCategory.InvalidArgument); + public static ErrorMessage NoMatchFoundForProvider = new ErrorMessage(Messages.NoMatchFoundForProvider, ErrorCategory.InvalidArgument); + public static ErrorMessage MethodNotImplemented = new ErrorMessage(Messages.MethodNotImplemented, ErrorCategory.NotImplemented); + public static ErrorMessage InstallRequiresCurrentUserScopeParameterForNonAdminUser = new ErrorMessage(Messages.InstallRequiresCurrentUserScopeParameterForNonAdminUser, ErrorCategory.NotImplemented); + public static ErrorMessage TooManyPackages = new ErrorMessage(Messages.TooManyPackages, ErrorCategory.InvalidArgument); + + // ReSharper restore InconsistentNaming + } + + internal static class Messages { +#if DEBUG + internal const string NotImplemented = "MSG:NotImplemented"; +#endif + internal const string InstallRequiresCurrentUserScopeParameterForNonAdminUser = "MSG:InstallRequiresCurrentUserScopeParameterForNonAdminUser"; + internal const string MoreThanOneFileMatched = "MSG:MoreThanOneFileMatched"; + internal const string FileNotFound = "MSG:FileNotFound"; + internal const string UnhandledException = "MSG:UnhandledException"; + internal const string RegisterPackageSourceRequired = "MSG:RegisterPackageSourceRequired"; + internal const string NoMatchFoundForProvider = "MSG:NoMatchFoundForProvider"; + internal const string FullProviderFilePathVersionNotAllowed = "MSG:FullProviderFilePathVersionNotAllowed"; + internal const string MultipleNamesWithVersionNotAllowed = "MSG:MultipleNamesWithVersionNotAllowed"; + internal const string WildCardCharsAreNotSupported = "MSG:WildCardCharsAreNotSupported"; + internal const string WhitespacesAreNotSupported = "MSG:WhitespacesAreNotSupported"; + internal const string MinimumVersionMustBeLessThanMaximumVersion = "MSG:MinimumVersionMustBeLessThanMaximumVersion"; + internal const string FolderNotFound = "MSG:FolderNotFound"; + internal const string MoreThanOneFolderMatched = "MSG:MoreThanOneFolderMatched"; + internal const string ActionInstallPackage = "MSG:ActionInstallPackage"; + internal const string ActionRegisterPackageSource = "MSG:ActionRegisterPackageSource"; + internal const string ActionReplacePackageSource = "MSG:ActionReplacePackageSource"; + internal const string ActionUninstallPackage = "MSG:ActionUninstallPackage"; + internal const string ActionUnregisterPackageSource = "MSG:ActionUnregisterPackageSource"; + internal const string BootstrapManualAssembly = "MSG:BootstrapManualAssembly"; + internal const string BootstrapManualInstall = "MSG:BootstrapManualInstall"; + internal const string BootstrapProvider = "MSG:BootstrapProvider"; + internal const string BootstrapProviderProviderRequested = "MSG:BootstrapProviderProviderRequested"; + internal const string BootstrapProviderUserRequested = "MSG:BootstrapProviderUserRequested"; + internal const string CaptionPackageContainsInstallationScript = "MSG:CaptionPackageContainsInstallationScript"; + internal const string CaptionPackageContainsUninstallationScript = "MSG:CaptionPackageContainsUninstallationScript"; + internal const string CaptionPackageInstallFailure = "MSG:CaptionPackageInstallFailure"; + internal const string CaptionPackageNotTrusted = "MSG:CaptionPackageNotTrusted"; + internal const string CaptionSourceNotTrusted = "MSG:CaptionSourceNotTrusted"; + internal const string CaptionPackageUninstallFailure = "MSG:CaptionPackageUninstallFailure"; + internal const string DestinationOrLiteralPathRequired = "MSG:DestinationOrLiteralPathRequired"; + internal const string ProviderNameNotSpecified = "MSG:ProviderNameNotSpecified"; + internal const string DestinationPathInvalid = "MSG:DestinationPathInvalid"; + internal const string DisambiguateForInstall = "MSG:DisambiguateForInstall"; + internal const string DisambiguateForUninstall = "MSG:DisambiguateForUninstall"; + internal const string FileNotRecognized = "MSG:FileNotRecognized"; + internal const string FilePathMustBeFileSystemPath = "MSG:FilePathMustBeFileSystemPath"; + internal const string InstallationFailure = "MSG:InstallationFailure"; + internal const string InstalledPackageMultiple = "MSG:InstalledPackageMultiple"; + internal const string InstallingPackageMultiple = "MSG:InstallingPackageMultiple"; + internal const string InstallingPackagesCount = "MSG:InstallingPackagesCount"; + internal const string MethodNotImplemented = "MSG:MethodNotImplemented"; + internal const string MatchesMultiplePackages = "MSG:MatchesMultiplePackages"; + internal const string MatchesMultipleProviders = "MSG:MatchesMultipleProviders"; + internal const string MustSpecifyCriteria = "MSG:MustSpecifyCriteria"; + internal const string NameOrLocationRequired = "MSG:NameOrLocationRequired"; + internal const string NoMatchesForWildcard = "MSG:NoMatchesForWildcard"; + internal const string NoMatchForProvidersAndSources = "MSG:NoMatchForProvidersAndSources"; + internal const string NoMatchFound = "MSG:NoMatchFound"; + internal const string NoMatchFoundForCriteria = "MSG:NoMatchFoundForCriteria"; + internal const string NoPackagesFoundForProvider = "MSG:NoPackagesFoundForProvider"; + internal const string OverwritingPackageSource = "MSG:OverwritingPackageSource"; + internal const string PackageInstallRequiresOption = "MSG:PackageInstallRequiresOption"; + internal const string PackageSourceExists = "MSG:PackageSourceExists"; + internal const string QueryBootstrap = "MSG:QueryBootstrap"; + internal const string QueryContinueInstallingAfterFailing = "MSG:QueryContinueInstallingAfterFailing"; + internal const string QueryContinueUninstallingAfterFailing = "MSG:QueryContinueUninstallingAfterFailing"; + internal const string QueryInstallUntrustedPackage = "MSG:QueryInstallUntrustedPackage"; + internal const string QueryShouldThePackageScriptAtBeProcessed = "MSG:QueryShouldThePackageScriptAtBeProcessed"; + internal const string QueryShouldThePackageUninstallScriptAtBeProcessed = "MSG:QueryShouldThePackageUninstallScriptAtBeProcessed"; + internal const string SavePackage = "MSG:SavePackage"; + internal const string SavePackageError = "MSG:SavePackageError"; + internal const string ShouldContinueWithUntrustedPackageSource = "MSG:ShouldContinueWithUntrustedPackageSource"; + internal const string SkippedInstalledPackage = "MSG:SkippedInstalledPackage"; + internal const string SkippedInstalledPackageMultiple = "MSG:SkippedInstalledPackageMultiple"; + internal const string SkippedProviderMissingRequiredOption = "MSG:SkippedProviderMissingRequiredOption"; + internal const string SourceFoundInMultipleProviders = "MSG:SourceFoundInMultipleProviders"; + internal const string SourceNotFound = "MSG:SourceNotFound"; + internal const string SourceNotFoundForLocation = "MSG:SourceNotFoundForLocation"; + internal const string SourceNotFoundForNameAndLocation = "MSG:SourceNotFoundForNameAndLocation"; + internal const string SourceNotFoundNoCriteria = "MSG:SourceNotFoundNoCriteria"; + internal const string SpecifiedProviderMissingRequiredOption = "MSG:SpecifiedProviderMissingRequiredOption"; + internal const string TargetPackage = "MSG:TargetPackage"; + internal const string TargetPackageVersion = "MSG:TargetPackageVersion"; + internal const string TargetPackageSource = "MSG:TargetPackageSource"; + internal const string UnableToFindProviderForSource = "MSG:UnableToFindProviderForSource"; + internal const string UninstallationFailure = "MSG:UninstallationFailure"; + internal const string UnknownProviders = "MSG:UnknownProviders"; + internal const string UnknownProviderFromActivatedList = "MSG:UnknownProviderFromActivatedList"; + internal const string PackageFileExists = "MSG:PackageFileExists"; + internal const string UnableToOverwrite = "MSG:UnableToOverwrite"; + internal const string ProviderFailToDownloadFile = "MSG:ProviderFailToDownloadFile"; + internal const string UnableToFindDependencyPackage = "MSG:UnableToFindDependencyPackage"; + internal const string VersionRangeAndRequiredVersionCannotBeSpecifiedTogether = "MSG:VersionRangeAndRequiredVersionCannotBeSpecifiedTogether"; + internal const string AllVersionsCannotBeUsedWithOtherVersionParameters = "MSG:AllVersionsCannotBeUsedWithOtherVersionParameters"; + internal const string InvalidVersion = "MSG:InvalidVersion"; + internal const string TooManyPackages = "MSG:TooManyPackages"; + } + + internal static class Nouns { + internal const string PackageNoun = "Package"; + internal const string PackageSourceNoun = "PackageSource"; + internal const string PackageProviderNoun = "PackageProvider"; + } + + internal static class Methods { + internal const string StopProcessingAsyncMethod = "StopProcessingAsync"; + internal const string ProcessRecordAsyncMethod = "ProcessRecordAsync"; + internal const string GenerateDynamicParametersMethod = "GenerateDynamicParameters"; + internal const string BeginProcessingAsyncMethod = "BeginProcessingAsync"; + internal const string EndProcessingAsyncMethod = "EndProcessingAsync"; + } + + internal static class Parameters { + internal const string ConfirmParameter = "Confirm"; + internal const string WhatIfParameter = "WhatIf"; + } + + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Utility/CustomRuntimeDefinedParameter.cs b/src/Microsoft.PowerShell.PackageManagement/Utility/CustomRuntimeDefinedParameter.cs new file mode 100644 index 000000000..f2aafb9bc --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Utility/CustomRuntimeDefinedParameter.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Utility { + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using System.Management.Automation; + using System.Security; + using Cmdlets; + using Microsoft.PackageManagement.Internal.Packaging; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + + internal class CustomRuntimeDefinedParameter : RuntimeDefinedParameter { + internal HashSet Options = new HashSet(); + + public CustomRuntimeDefinedParameter(DynamicOption option, bool isInvocation, IEnumerable parameterSets ) + : base(option.Name, ActualParameterType(option.Type), new Collection()) { + if (isInvocation) { + Attributes.Add(new ParameterAttribute()); + } else { + IncludeInParameterSet(option, isInvocation, parameterSets); + } + + Options.Add(option); + var values = option.PossibleValues.ToArray(); + if (!values.IsNullOrEmpty()) { + Attributes.Add(new ValidateSetAttribute(values)); + } + } + + public void IncludeInParameterSet(DynamicOption option, bool isInvocation, IEnumerable parameterSets) { + foreach (var ps in parameterSets) { + var parameterSetName = !string.IsNullOrWhiteSpace(ps) ? option.ProviderName + ":" + ps : option.ProviderName; + if (Attributes.Select(each => each as ParameterAttribute).WhereNotNull().Any(each => each.ParameterSetName == parameterSetName)) { + continue; + } + Attributes.Add( + new ParameterAttribute() { + ParameterSetName = parameterSetName, + Mandatory = option.IsRequired + }); + } + } + + internal bool IsRequiredForProvider(string name) { + return Options.Any(each => each.ProviderName.EqualsIgnoreCase(name) && each.IsRequired); + } + + internal IEnumerable GetValues(AsyncCmdlet cmdlet) { + if (IsSet && Value != null) { + switch (Options.FirstOrDefault().Type) { + case OptionType.Switch: + return new[] { + ((SwitchParameter)Value).IsPresent.ToString() + }; + case OptionType.StringArray: + return (string[])Value; + + case OptionType.File: + return new[] { + cmdlet.ResolveExistingFilePath(Value.ToString()) + }; + + case OptionType.Folder: + return new[] { + cmdlet.ResolveExistingFolderPath(Value.ToString()) + }; + + case OptionType.Path: + return new[] { + cmdlet.ResolvePath(Value.ToString()) + }; + +#if !CORECLR + case OptionType.SecureString: + return new[] { + "SECURESTRING:" + ((SecureString)Value).ToProtectedString("salt") + }; +#endif + } + return new[] { + Value.ToString() + }; + } + return new string[0]; + } + + private static Type ActualParameterType(OptionType optionType) { + switch (optionType) { + case OptionType.Switch: + return typeof (SwitchParameter); + case OptionType.Uri: + return typeof (Uri); + case OptionType.StringArray: + return typeof (string[]); + case OptionType.Int: + return typeof (int); + case OptionType.Path: + return typeof (string); + case OptionType.File: + return typeof (string); + case OptionType.Folder: + return typeof (string); + case OptionType.SecureString: + return typeof (SecureString); + + default: + return typeof (string); + } + } + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Utility/ErrorMessage.cs b/src/Microsoft.PowerShell.PackageManagement/Utility/ErrorMessage.cs new file mode 100644 index 000000000..87a6cabd7 --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Utility/ErrorMessage.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Utility { + using System.Management.Automation; + + internal class ErrorMessage { + public readonly ErrorCategory Category; + public readonly string Resource; + + public ErrorMessage(string resource, ErrorCategory category) { + Resource = resource; + Category = category; + } + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Utility/HostApiExtensions.cs b/src/Microsoft.PowerShell.PackageManagement/Utility/HostApiExtensions.cs new file mode 100644 index 000000000..8a9befa73 --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Utility/HostApiExtensions.cs @@ -0,0 +1,96 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +namespace Microsoft.PowerShell.PackageManagement.Utility { + using System; + using System.Diagnostics.CodeAnalysis; + using Microsoft.PackageManagement.Implementation; + using Microsoft.PackageManagement.Internal.Api; + using Microsoft.PackageManagement.Internal.Implementation; + using Microsoft.PackageManagement.Internal.Utility.Extensions; + using Microsoft.PackageManagement.Internal.Utility.Plugin; + + /// + /// This can be used when we want to override some of the functions that are passed + /// in as the implementation of the IHostApi (ie, 'request object'). + /// Because the DynamicInterface DuckTyper will use all the objects passed in in order + /// to implement a given API, if we put in delegates to handle some of the functions + /// they will get called instead of the implementation in the current class. ('this') + /// + + internal static class HostApiExtensions { + internal static IHostApi ProviderSpecific(this IHostApi parent, PackageProvider provider) { + var thisProviderIsCanceled = false; + return new object[] { + new { + Error = new Func((id, cat, targetobjectvalue, messageText) => { + // turn errors into warnings when we're working in parallel + parent.Warning(messageText); + + thisProviderIsCanceled = true; + // tell the provider that yeah, this request is canceled. + return thisProviderIsCanceled; + }), + + GetIsCanceled = new Func(() => parent.IsCanceled || thisProviderIsCanceled) + }, + parent, + }.As(); + } + + internal static IHostApi SuppressErrorsAndWarnings(this IHostApi parent, bool isProcessing) { + + return new object[] { + new { + Error = new Func((id, cat, targetobjectvalue, messageText) => { +#if DEBUG + parent.Verbose("Suppressed Error {0}".format(messageText)); +#endif + return false; + }), + Warning = new Func((messageText) => { +#if DEBUG + parent.Verbose("Suppressed Warning {0}".format(messageText)); +#endif + return true; + }), + Verbose = new Func((messageText) => { + if (isProcessing) { + parent.Verbose(messageText); + } +#if DEBUG + else { + parent.Verbose("Suppressed Verbose {0}".format(messageText)); + } +#endif + return true; + }), + + }, + parent, + }.As(); + } + + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Required.")] + internal static IHostApi SuprressBootstrapping(this IHostApi parent, bool isProcessing) { + return new object[] { + new { + IsInvocation = new Func(() => false), + ShouldBootstrapProvider = new Func((s1, s2, s3, s4, s5, s6) => false), + IsInteractive = new Func(() => false), + }, + parent, + }.As(); + } + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Utility/ProgressTracker.cs b/src/Microsoft.PowerShell.PackageManagement/Utility/ProgressTracker.cs new file mode 100644 index 000000000..a23916230 --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Utility/ProgressTracker.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Utility { + using System.Collections.Generic; + + internal class ProgressTracker { + internal string Activity; + internal List Children = new List(); + internal int Id; + internal ProgressTracker Parent; + } +} \ No newline at end of file diff --git a/src/Microsoft.PowerShell.PackageManagement/Utility/ValidateListAttribute.cs b/src/Microsoft.PowerShell.PackageManagement/Utility/ValidateListAttribute.cs new file mode 100644 index 000000000..2e14f0824 --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Utility/ValidateListAttribute.cs @@ -0,0 +1,83 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Microsoft.PowerShell.PackageManagement.Utility { + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Management.Automation; + using System.Text; + + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public class ValidateListAttribute : ValidateEnumeratedArgumentsAttribute { + private readonly List _validValues; + private bool ignoreCase = true; + + public ValidateListAttribute(params string[] validValues) { + validValues = validValues ?? new string[0]; + this._validValues = validValues.ToList(); + } + + public bool IgnoreCase { + get { + return this.ignoreCase; + } + set { + this.ignoreCase = value; + } + } + + public IList ValidValues { + get { + return new List { + "green" + }; + } + } + + public static implicit operator ValidateSetAttribute(ValidateListAttribute vla) { + return new ValidateSetAttribute(vla._validValues.ToArray()); + } + + protected override void ValidateElement(object element) { + if (element == null) { + throw new ValidationMetadataException("ArgumentIsEmpty"); + } + var strB = element.ToString(); + + if (this._validValues.Any(t => string.Compare(t, strB, StringComparison.OrdinalIgnoreCase)== 0)) { + return; + } + throw new ValidationMetadataException("ValidateSetFailure"); + } +#if NOT_NEEDED + private string SetAsString() { + var validateSetSeparator = ","; + var stringBuilder = new StringBuilder(); + if (this._validValues.Count > 0) { + foreach (var str in this._validValues) { + stringBuilder.Append(str); + stringBuilder.Append(validateSetSeparator); + } + stringBuilder.Remove(stringBuilder.Length - validateSetSeparator.Length, validateSetSeparator.Length); + } + return ((object)stringBuilder).ToString(); + } + + //,ValidateList("One","Two", "Three") + +#endif + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/Utility/WebProxy.cs b/src/Microsoft.PowerShell.PackageManagement/Utility/WebProxy.cs new file mode 100644 index 000000000..b1d0d99bf --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/Utility/WebProxy.cs @@ -0,0 +1,46 @@ +using System; +using System.Net; + +namespace Microsoft.PowerShell.PackageManagement.Utility +{ + /// + /// Used by OneGet cmdlet to supply webproxy to provider + /// We cannot use System.Net.WebProxy because this is not available on CoreClr + /// + internal class InternalWebProxy : IWebProxy + { + Uri _proxyUri; + ICredentials _credentials; + + public InternalWebProxy(Uri uri, ICredentials credentials) + { + Credentials = credentials; + _proxyUri = uri; + } + + /// + /// Credentials used by WebProxy + /// + public ICredentials Credentials + { + get + { + return _credentials; + } + set + { + _credentials = value; + } + } + + public Uri GetProxy(Uri destination) + { + return _proxyUri; + } + + public bool IsBypassed(Uri host) + { + return false; + } + } +} diff --git a/src/Microsoft.PowerShell.PackageManagement/resources/Microsoft.PowerShell.PackageManagement.Resources.Messages.resx b/src/Microsoft.PowerShell.PackageManagement/resources/Microsoft.PowerShell.PackageManagement.Resources.Messages.resx new file mode 100644 index 000000000..8b0deef78 --- /dev/null +++ b/src/Microsoft.PowerShell.PackageManagement/resources/Microsoft.PowerShell.PackageManagement.Resources.Messages.resx @@ -0,0 +1,523 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Do you wish to continue installing packages? + query + + + Do you wish to continue uninstalling packages? + query + + + The desintation path '{0}' for package '{1}' is invalid. + {0} - path +{1} - package name + + + Unable to install, multiple packages matched '{0}'. {1} + {0} - search criteria, {1} - suggestion + + + Unable to uninstall, multiple packages matched '{0}'. + {0} - search criteria + + + The package source '{0}' was found in multiple providers ({1}). + {0} package source name, {1} list of package providers + + + Skipping package provider provider '{0}'-- missing required option '{1}'. + {0} - provider name, {1} - dynamic option name + + + Package '{0}' failed to install. + {0} -package name + + + The package '{0}' comes from a package source that is not marked as trusted. + {0} package name + + + The package(s) come(s) from a package source that is not marked as trusted. + package source not trusted + + + Install Package + action 'install package' + + + '{0}' matched package '{2}/{3}' from provider: '{1}', source '{4}'. + {0} search criteria {1} provider {2} package name {3} package version {4} package sources + + + Package Source '{0}' ({1}) in provider '{2}'. + {0} package source name , {1} package source location, {2} provider name + + + No package found for '{0}'. + {0} - search criteria + + + No match was found for the specified search criteria and package name '{0}'. Try Get-PackageSource to see all available registered package sources. + {0} - package name + + + The package provider '{0}' did not return any packages. + {0} - provider name + + + Package '{0}' contains an installation script. + {0} package name + + + Package '{0}' contains an uninstallation script. + {0} package name + + + File '{0}' is not recognized as a valid package. + {0} file name + + + Package '{0}' failed to install. + {0} package name + + + Package '{0}' failed to uninstall. + {0} - package name + + + Should the package install script at '{0}' be processed? + {0} - package script location + + + Should the package uninstall script at '{0}' be processed? + {0} - package script location + + + Unable to find package source '{0}'. Use Get-PackageSource to see all available package sources. + {0} package source name + + + Are you sure you want to install software from '{1}'? + {0} package name, {1} package source + + + Package {0} failed to uninstall. + {0} package name + + + Uninstall Package. + action 'uninstall package' + + + Unable to find package provider '{0}'. + {0} - package provider name + + + '{0}' may be manually downloaded from '{1}' and copied to '{2}'. + {0} -provider name, {1} url, {2} destination folder + + + {0} may be manually downloaded from {1} and installed. + {0} -provider name, {1} url + + + {0} +{1} + {0} Request Message +{1} Manual Instructions + + + The provider '{0}' requires provider '{1} v{2}' to continue. + {0} requesting provider {1} provider name {2} provider version + + + The provider '{0} v{1}' is not installed. + {0} provider name {1} provider version + + + Would you like PackageManagement to automatically download and install '{0}' now? + {0} -provider name + + + Package '{0}' version '{1}' from '{2}'. + 0- name +1 - version +2 - package source + + + Package '{0}' with version '{1}'. + 0- version + 1 - package + + + Register Package Source. + + + Replace Package Source. + action 'replace package source' + + + Unregister Package Source. + action unregister package source + + + Not Implemented. + This should only show in DEBUG builds + + + Overwriting package source '{0}'. + (verbose warning ) - {0} - package source name + + + Package '{0}' from package provider '{1}' requires the '{2}' parameter to install. + {0} package name, {1} package provider name, {2} parameter name + + + Package Source '{0}' exists. + {0} package source name + + + Unable to find package source for location '{0}'. + {0} location + + + Unable to find package source for name '{0}' location '{1}'. + {0} name, {1} location + + + Unable to find package sources. + + + Unable to find package provider for package source '{0}'. + {0} - package source name + + + Saving a package requires either a -Path or -LiteralPath parameter. + + + Specified ProviderName parameter matches multiple providers: {0}. + {0} - comma seperated list of providers matched + + + ProviderName must be specified. Available Providers: {0}. + {0} - comma seperated list of available providers + + + Either -Name or -Location must be specified to select a package source. + + + No package found matching '{0}'. + {0} wildcard match string + + + No combination of providers ({0}) match sources specified ({1}). + 0 provider names, 1- source names + + + Save Package + action 'save package' + + + ??? not used ??? + + + User declined to install untrusted package ({0}). + provider names + + + Unable to find package providers ({0}). + provider names + + + Package file '{0}' exists. Remove the file first or use -Force to overwrite. + {0} - package filename + + + Unable to remove package file '{0}'. + {0} - package filename + + + The path '{0}' must refer to a single file system path. + {0} provides the path supplied by the user + + + Error running Save-Package cmdlet: {0}. + + + Unable to download the list of available providers. Check your internet connection. + + + Install-Package requires parameters to select a package to install. + + + Unable to find dependent package(s) ({0}) + {0} canonical package id(s). + + + Network connectivity may not be available, unable to reach remote sources. + + + Installed Package '{0}' ({1} of {2}). + + + Installing Package '{0}' ({1} of {2}). + + + Installing {0} packages. + package count + + + Skipping installed package {0} {1}. + name, version + + + Skipping installed package '{0}' ({1} of {2}). + + + You cannot use the parameters RequiredVersion and either MinimumVersion or MaximumVersion in the same command. + + + The action with the specified provider '{0}' is missing one or more required parameters: {1}. + 0 -provider name, 1- comma seperated list of required missing parameters + + + Cannot find file '{0}'. + {0} - file name or path. + + + Multiple matches found '{0}' for the specified file '{1}'. + 0 - matched files, 1 - file path. + + + Multiple matches found '{0}' for the specified folder '{1}'. + 0 - matched folders, 1 - folder path. + + + Unhandled Exception - Message:'{0}' Name:'{1}' Stack Trace:'{2}' + 0 - Message, 1 - Name, 2 - stack trace. + + + The specified MinimumVersion '{0}' is greater than the specified MaximumVersion '{1}' + + + The version parameter is allowed only when a single module name is specified as the value of the Name parameter, without any wildcard characters. + + + You cannot use the RequiredVersion and either MinimumVersion or MaximumVersion in the same command. Specify only one of these parameters in your command. + + + The Version parameter is allowed only when a single provider name is specified as the value of the Name parameter, without any wildcard characters. The file path is not allowed with version parameter. + + + Unable to find package provider '{0}'. It may not be imported yet. Try 'Get-PackageProvider -ListAvailable'. + {0} - provider name + + + You cannot use the parameter AllVersions with RequiredVersion, MinimumVersion or MaximumVersion in the same command. + + + No match was found for the specified search criteria for the provider '{0}'. The package provider requires 'PackageManagement' and 'Provider' tags. Please check if the specified package has the tags. + {0} - provider name + + + Specify an exact Name and RequiredVersion parameter. + + + See Get-PackageSource for all available registered sources and try again by specifying a single Source parameter. + + + Unable to find repository with SourceLocation '{0}'. Use Get-PSRepository to see all available repositories. + {1} package source + + + The specified version '{0}' is invalid. Error: '{1}'. + 0 - version string, 1 - error message + + + Using the provider '{0}' for searching packages. + 0 - provider name + + + Unable to save the package '{0}'. + 0 - package name + + + Folder '{0}' cannot be found + + + The specified name '{0}' should not contain any wildcard characters, please correct it and try again. + 0 - package or provider name + + + Please specify an exact -Name and -RequiredVersion. + + + Please specify a single -ProviderName. + + + Please specify a single -Source. + + + Provider '{0}' does not implement '{1}'. + 0 - provider name. 1 - method name (such as save-package) + + + Administrator rights are required to install packages in '{0}'. Log on to the computer with an account that has Administrator rights, and then try again, or install in '{1}' by adding "-Scope CurrentUser" to your command. You can also try running the Windows PowerShell session with elevated rights (Run as Administrator). + 0 - folder, 1 - folder + + + PackageManagement cannot handle '{0}' packages.The limit is 63. + 0 - number of packages + + + PackageManagement: A package is installed. + Please do not localize PackageManagement + + + PackageManagement: A package is saved. + Please do not localize PackageManagement + + + PackageManagement: A package is uninstalled. + Please do not localize PackageManagement + + + '{0}' to location '{1}' + 0 - package name, 1 - save location + + + Imported provider '{0}' . + 0 - provider name + + + Import-PackageProvider failed. Possibly the provider name is different from the package name '{0}'. Try Get-PackageProvider -ListAvailable to identify the associated provider name and run Import-PackageProvider". + + + Completed + + + Processing + + + The specified name '{0}' should not be whitespaces only, please correct it and try again. + 0 - package or provider name + + \ No newline at end of file