Monday, July 20, 2020

Azure/PowerShell: Virtual Machines created from Image, Adding Applications to the Taskbar

Once an Azure virtual machine is created from an image, users can be added either local users or by adding the machine to a domain. As part of user setup, it is convenient to add applications to taskbar from a user. PowerShell is just the automation tool to handle this task.

The Taskbar is part of Windows Shell (thinking 1990s COM). It is no surprise that the code to add an application to the Taskbar requires accessing a COM object:
        $shellApplication = New-Object -ComObject shell.application
        $taskbarApplicaxtion = $shellApplication.Namespace($directory).ParseName($candidate.Name) 
        $taskbarApplicaxtion.invokeverb('taskbarpin')


The PowerShell script to add the following applications to the TaskBar is below:
  • Chrome
  • Edge
  • Notepad++
  • Visual Studio Code
  • Visual Studio
  • SQL Server Management Studio
The PowerShell script is as follows:

[string] $shellApplicationVerbTaskbarPin = 'taskbarpin'

function Add-TaskbarApplication() {
    param (
        [Parameter(Mandatory = $true)]
        [string] $applicationExecutable,
        [Parameter(Mandatory = $false)]
        [string] $applicationCandidatePath = ${env:ProgramFiles(x86)}
    )

    $candidates = Get-ChildItem `
        -Path $applicationCandidatePath `
        -Filter $applicationExecutable `
        -Recurse `
        -ErrorAction SilentlyContinue `
        -Force
    if ($null -eq $candidates) {
        # Application not found
        return 
    }

    foreach ($candidate in $candidates) {
        [string] $directory = Split-Path -Path $candidate.FullName

        $shellApplication = New-Object -ComObject shell.application
        $taskbarApplicaxtion = $shellApplication.Namespace($directory).ParseName($candidate.Name)
        if ($null -eq $taskbarApplicaxtion) {
            continue
        }
    
        $taskbarApplicaxtion.invokeverb($shellApplicationVerbTaskbarPin)
        break # certain files like msedge.exe are in multiple locations so only one pinning
    }
}

Add-TaskbarApplication 'chrome.exe' 
Add-TaskbarApplication 'msedge.exe'
Add-TaskbarApplication 'notepad++.exe'
# Visual Studio Code
Add-TaskbarApplication 'code.exe' $env:LOCALAPPDATA
# Visual Studio
Add-TaskbarApplication 'devenv.exe'
# SQL Server Management Studio
Add-TaskbarApplication 'ssms.exe'



Sunday, July 19, 2020

Azure/PowerShell: Virtual Machines created from Images, Cleaning cached up SQL Server Host Names

As part of my DevOps role, I have created a set of standard images to be used by our QA engineers in creating test Virtual Machines on Azure. During setup, the QA lead reported that it was impossible to login to a SQL Server instance running on the virtual machine. The client being used to login to the instance of SQL Server was SQL Server Management Studio (SSMS). I investigated and found out that SSMS remembered the Server Name (host name) of the virtual machine from which the Azure image was created. The fix was simple. The QA lead needed to enter the current computer name of the virtual machine.

This was a usability issues that merited fixing. To explain more clearly assume the virtual machine that was used to create the Azure image was named, SrcHost4Image. This source virtual machine was running SQL Server. If a new Virtual Machine named, NewVM001, is created from the image, SSMS will show the origin host name:




The host, SrcHost4Image, no longer exists so there is no way to login with those SQL Server credentials.

The file where SSMS stores its most recently used server names should be updated. In earlier incarnations of SSMS that most recently used (MRU) server names were stored in a binary file, mru.dat for SQL Server Management Studio 2005 and SqlStudio.bin for more recent incarnations of SSMS. SQL Server Management Studio has moved to an XML file, UserSettings.xml, in order to store the MRU server names. On most configurations of Windows this file is found under:

C:\Users\%username%\AppData\Roaming\Microsoft\
    SQL Server Management Studio\18.0

The XML to be modified from the UserSettings.xml configuration file was of the form:
<#
<ServerConnectionItem>
    <Instance>SrcHost4Image</Instance>
    <AuthenticationMethod>0</AuthenticationMethod>
    <Connections>
    <Element>
        <Time>
        <long>-637307108466504502</long>
        </Time>
        <Item>
        <ServerConnectionSettings>
            <Instance>SrcHost4Image</Instance>
            <UserName>SrcHost4Image\Jan Narkiewicz</UserName>
#>

The PowerShell modifying the two <Instance> XML elements and the <UserName> XML element is as follows:

# $env:LOCALAPPDATA = C:\Users\Jan Narkiewicz\AppData\Roaming
# PowerShell 7 style Join-Path
[string] $userSettingsPath = Join-Path `
    $env:APPDATA 'Microsoft\SQL Server Management Studio\18.0'
[string] $userSettingsFilename = 'UserSettings.xml'
[string] $userSettingsFullFilename = 
    Join-Path $userSettingsPath $userSettingsFilename

function Get-FirstElement($elementCandidate)
{
    if ($elementCandidate.Count -eq 0)
    {
        return $null
    }
        
    elseif ($elementCandidate.Count -eq 1)
    {
        # if one element is found then no array is created 
        # (just an object)
        return $elementCandidate
    }
    
    elseif ($elementCandidate.Count -gt 1)
    {
        # if more than one element is found then an array is created 
        # (select 0th element)
        return $elementCandidate[0]
    }    
}

[xml]$settings = Get-Content $userSettingsFullFilename

<#
<ServerConnectionItem>
    <Instance>SrcHost4Image</Instance>
    <AuthenticationMethod>0</AuthenticationMethod>
    <Connections>
    <Element>
        <Time>
        <long>-637307108466504502</long>
        </Time>
        <Item>
        <ServerConnectionSettings>
            <Instance>SrcHost4Image</Instance>
            <UserName>SrcHost4Image\Jan Narkiewicz</UserName>
#>

$serverTypesElements = $settings.SqlStudio.SSMS.ConnectionOptions.ServerTypes.Element
if ($serverTypesElements.Count -eq 0)
    exit
}

[System.Xml.XmlElement] $serverTypeItemElement

$serverTypeItemElement = Get-FirstElement $serverTypesElements[0].Value.ServerTypeItem.Servers.Element
if ($serverTypeItemElement -eq $null)
{
    exit
}
    
[System.Xml.XmlElement] $serverConnectionItem = $serverTypeItemElement.Item.ServerConnectionItem
 
$serverConnectionItem.Instance = $env:computername
    
[System.Xml.XmlElement] $serverConnectionSettings = $serverConnectionItem.Element.ServerConnectionSettings

$serverConnectionSettings.Instance = $env:computername
$serverConnectionSettings.UserName = $env:computername + '\'+ $env:UserName

$settings.Save($userSettingsFullFilename)

Appendix A: Locations of SSMS User Settings Files

To force SSMS to forget legacy for older versions of SSMS, the user settings file (a binary file) should be deleted. This sections lists the legacy configuration files used by SQL Server Management Studio:

SQL Server Management Studio 2005: 
C:\Users\%username%\AppData\Roaming\Microsoft\
    Microsoft SQL Server\90\Tools\Shell\mru.dat

SQL Server Management Studio 2008: 
C:\Users\%username%\AppData\Roaming\Microsoft\
    Microsoft SQL Server\100\Tools\Shell\SqlStudio.bin

SQL Server Management Studio 2012: 
C:\Users\%username%\AppData\Roaming\Microsoft\
    SQL Server Management Studio\11.0\SqlStudio.bin

SQL Server Management Studio 2014: 
C:\Users\%username%\AppData\Roaming\Microsoft\
    SQL Server Management Studio\12.0\SqlStudio.bin

SQL Server Management Studio 2016: 
C:\Users\%username%\AppData\Roaming\Microsoft\
    SQL Server Management Studio\13.0\SqlStudio.bin

SQL Server Management Studio 2017:
C:\Users\%username%\AppData\Roaming\Microsoft\
    SQL Server Management Studio\14.0\SqlStudio.bin

Sunday, July 12, 2020

Creating Windows Shortcuts (links) with .NET Core and .NET Standard

I was tasked with creating a shortcut in a C# application. As a technology goal, it was desired to use .NET Standard so the code could be more readily updated to .NET Core/.NET 5 at a future time.  There are numerous examples of creating and managing file shortcuts programmatically with C# and the .NET Framework. One such example is: Create shortcut programmatically in C#. All such examples use COM and the Windows Script Host Object Model to access the Windows Shell (the system that creates and manages shortcuts). COM interop works for the .NET Framework but does not work for .NET Core or .NET Standard.

The shortcut was to be same across every computer on which the software was installed such as:
  • c:\bin\nuget.exe: executable to invoke when shortcut is clicked on
  • c:\SomeFolder\nuget.lnk: location of the shortcut that invokes c:\bin\nuget.exe
The problem to be solved is not about creating a shortcut. That can be done manually on Windows (see Windows How to Create/Copy a File Shortcut using File Explorer). The problem is solved by copying the shortcut file once created to the new machine. To make life simpler, the shortcut file should be renamed so that it could be written with the *.lnk extension but could be managed as a generic binary file (extension *.bytes) so that Windows does not attempt to treat the file as a shortcut. 

Developers who need to create a custom shortcut (custom target, custom name, etc.) using .NET Standard or .NET Core should stop reading. That functionality is only available using .NET Framework and COM interop and this style of development is not presented as part of this blog.

Recall in Windows How to Create/Copy a File Shortcut using File Explorer that a shortcut could be uploaded to Google Drive. As Google Drive is not Windows there was no attempt to follow the shortcut to the target folder so just the file associated with shortcut was updated:


Changing the extension from *.lnk to *.bytes on a Widows computer requires custom Windows Shell coding, A simpler approach is to rename the file from nuget.exe.lnk to nuget.exe.bytes while it is stored on Google Drive:


The nuget.exe.bytes file can be downloaded from Google Drive to the Downloads folder and Windows will not interpret the file as a shortcut. 

To demonstrate that a .NET Standard assembly can create a shortcut, a class library named ExampleDotNetStardardLibrary was created. The way that the shortcut will be included in this assembly is by using a resource file. A resource file is added to a C# project by clicking on the project in Solution Explorer and selecting Add | New Item:


Selecting Add | New Item displays the Add New Item dialog. Once the Add New Item dialog is displayed, the term "resource" can be typed in the textbox used to filter the file types displayed. This will reveal the Resource File file type as follows:


Click add which adds a Resource File named Resource1.resx to the .NET Standard class library project. To rename the Resource File, Resoure1.resx, right click on it and select Rename:


The resource file can be renamed ShortcutManager.resx. Double clicking on the resource file, ShortcutManager.resx, displays a designer that includes a menu entitled Add Resource. Clicking on the down arrow of Add Resource allows the Add Existing File menu item to be selected:


Invoking the Add Existing File option displays the Add existing file to resource dialog. Using this dialog it is possible to navigate to the Downloads folder where nuget.exe.bytes was downloaded from Google drive:


Once nuget.exe.bytes is selected, click on the Open button to add the file to the resource file: The nuget.exe.bytes file will be stripped of its extension (*.bytes) and stored in the project's Resources folder as follows:


Double clicking on the ShortcutManager.Designer.cs displays the code generated by Visual Studio that allows the byte associated with the nuget.exe resource to be access (remember this a shortcut not the executable):


The nuget_exe property can be invoked in order to access the bytes associated with the shortcut. This property has protection level internal so it can only be accessed by code internal to the Standard Library containing the resource file.

The ShortcutManager class, like the nuget_exe property, is set to protection level internal so the calls's methods and properties cannot be invoked from outside its .NET Standard class library. The following class, Shortcut, with protection level public demonstrates how to save the shortcut to a folder thus showing .NET Standard can be used to create a shortcut:

namespace ExampleDotNetStandardLibrary
{
    using System.IO;

    public class Shortcut
    {
        public static void Save()
        {
            File.WriteAllBytes(
                "C:\\SomeFolder\\nuget.exe.lnk", 
                 ShortcutManager.nuget_exe);
        }
    }
}







Saturday, July 11, 2020

Windows How to Create/Copy a File Shortcut using File Explorer

As with many of blog posts on this site, I create a rudimentary, background post for a more complex development or devops issue to be addressed in a future post. Today's topic is just such a rudimentary blog post and presents a basic Windows concept, namely how to create a shortcut. 

The file to which the shortcut will link will be c:\bin\nuget.exe which shows File Explorer as having a size of 6,424 KB:


Shortcuts are not limited to executable files like nuget.exe. A shortcut or link can be created to any file type in Windows or shortcuts can point to folders.

To create a shortcut, using File Explorer, navigate to the folder in which the shortcut is to be created. Right click on the folder and select New | Shortcut from the context menu displayed by the right click action:


When New | Shortcut is selected, the Create Shortcut wizard will be displayed as follows:



Click on the Browse button to select the source file via the Browse for Files or folders dialog:


The title of the dialog, Browse for Files or Folders, is a clear indication that shortcuts can reference files or folders. Using the dialog, navigate to the file to which the shortcut will reference;



Click on OK which closes the Browse for Files and Folder dialog:


Click on Next which allows the shortcut to be named:



Click on Finish which shows the folder (C:\SomeFolder) containing the newly created shortcut:



Recall that the actually file, c:\bin\nuget.exe, is 6,424 KB. The shortcut is 1 KB (actually less) and demarcated by an icon indicating it is a link. Under the covers, a shortcut is just a file referencing a file stored in a different location.

It is possible to copy a shortcut to a different folder either using File Explorer's context menu or using CTRL+C, CTRL+V. Below is an example of the shortcut, C:\SomeFolder\nuget.exe, copied to a different location:


The shortcut versus the file referenced by the shortcut can even be copied to Google Drive:


Notice the file size is 866 bytes indicating the shortcut was copied and not the 6,624 KIB original file. Obviously, the shortcut stored on Google Drive's cloud storage can't invoke c:\bin\nuget.exe.

One reason to copy the shortcut cloud storage is that the shortcut could be download and used on a different computer. As long as the destination file (c:\bin\nuget.exe) resides on the same location on a different Windows machine, the shortcut can be downloaded and used on a different Windows machine.







Tuesday, July 7, 2020

PowerShell/Nuget: create a NuSpec file Containing all Files in a Folder

For a project, I needed a Nuget package that contained all files in a given folder hierarchy. This meant that the Nuget packaged included assemblies (DLLs) and other files that were to be considered content. A luck would have it, my last blog post contained an example of how, given a folder name passed as a parameter, to traverse every file a sub-folder: PowerShell: Traverse all Folders and Files for a given Path.

Files are specified in a Nuspec file using the falling rather mundane XML format where a <files> element contains one ore more <file> elements. Wild cards are supported as are attributes to include and exclude specific files. The release notes found at NuGet 1.5 Release Notes contains the following documentation:


The PowerShell script for traversing a folder hierarchy and creating on <file> element per-detected fie is as follows:

Param (
    [string] $folderName = $PSScriptRoot
)

class NuSpecFiles {
    [System.Text.StringBuilder] $filesXml = [System.Text.StringBuilder]::new()

    [string] $rootFolder

    [void] Append([string] $parentFolder, [string] $filename) {
        [string] $target = $filename

        if (-not ([string]::IsNullOrEmpty($parentFolder))) {
            $target = Join-Path -Path $parentFolder -ChildPath $filename            
        }

        # <file src="Your_Folder\*.pp" target="content\Your_Folder"/>
        $this.filesXml.AppendLine("  <file src=""$parentFolder"" target=""$target"" />")
    }

    [string] StripParentFolder([string] $folder) {
        return $folder.TrimStart($this.rootFolder)
    }

    [void] Traverse([string] $folderName) {
        [string] $parentFolder = $this.StripParentFolder($folderName)

        $entities = Get-ChildItem `
            -Path $folderName `
            -ErrorAction SilentlyContinue `
            -Force

        foreach ($entry in $entities) {
            if ($entry -is  [System.IO.DirectoryInfo]) {
                $this.Traverse($entry.FullName)
            }

            else { # if ($entry -is  [System.IO.FileInfo])
                $this.Append($parentFolder, $entry.Name)                
            }
        }
    }

    <#
        <files>
            <file src="lib\net472\any_assembly.dll" 
                  target="lib\net472" />
            <file src="Any_Folder\*.txt"               
                  target="content\Any_Folder"/>
            <file src="Any_Folder\Any_SubFolder\*.*" 
                  target="content" />
            <file src="*.config" target="content" />
        </files>
    #>
    [void] Create([string] $folderName)
    {
        $this.rootFolder = $folderName
        $this.filesXml.Clear()
        $this.filesXml.AppendLine('<files>')
        $this.Traverse($folderName)
        $this.filesXml.AppendLine('</files>')
    }

    [string] GetFilesXmlElement()
    {
        return $this.filesXml.ToString()
    }
}

[NuSpecFiles] $nuspecFiles = [NuSpecFiles]::new()

$nuspecFiles.Create($folderName)
$nuspecFiles.GetFilesXmlElement()

PowerShell: Traverse all Folders and Files for a given Path

For different projects, I keep writing the same code again. For a given directory, the code should recursively traverse the directory and potentially take some action. This is the kind of code that you write in your first computer science class but it takes me ten to fifteen minutes each time I rewrite. If I blog about it I can cut and paste it far into the future.

Here is an example of the PowerShell script is invoked:
.\Folder.ps1 -FolderName C:\Blog

Here is the PowerShell to traverse a given folder:

Param (
    [string] $folderName = $PSScriptRoot
)

class Folder
{
    [void] Traverse([string] $folderName)
    {
        $entities = Get-ChildItem `
            -Path $folderName `
            -ErrorAction SilentlyContinue `
            -Force

        foreach ($entry in $entities) {
            if ($entry -is  [System.IO.DirectoryInfo]) {
                Write-Host 'Directory:' $entry.FullName
                $this.Traverse($entry.FullName)
            }

            else { # if ($entry -is  [System.IO.FileInfo])
                Write-Host 'File:' $entry.FullName                
            }
        }
    }
}

[Folder] $folder = [Folder]::new()

$folder.Traverse($folderName)



Saturday, July 4, 2020

PowerShell: Installing .NET 3.5 on a stock Azure VM running Windows Server 2012 R2

To handle a pressing issue I needed to setup an environment running the following on an Azure VM.
  • Windows Server 2012 R2
  • SQL Server 2014
  • SharePoint Server 2013 R2
The Windows Server 2012 R2 image provided by Azure's VM marketplace did not come with .NET Framework 3.5 installed. SQL Server 2014 and SharePoint Server 2013 R2 both require .NET Framework 3.5.

Using Server Manager on the Windows Server 2012 R2 host to install .NET Framework 3.5 results in the following error after the Install button pushed on the final configuration screen:


Microsoft provides the following article, Enable .NET Framework 3.5 by using Windows PowerShell, explaining how to install .NET Framework 3.5 on Windows Server 2012 R2. 

The first step required to install the .NET Framework 3.5  is to download a Windows Server 2012 R2 installation image. The image contains a folder Source\SXS which has the files required to install .NET Framework 3.5. Those with access to a Visual Studio Enterprise Subscription can login to the following URL and select the Downloads menu:

After the Downloads menu is selected, enter Windows Server 2012 R2 in the search box. Clicking on the Search button will reveal the requisite download:


Once the ISO (DVD image) is downloaded, click on the file in Windows Explorer which mounts the image on operating systems like Windows 10. On my machine the ISO for Windows Server 2012 mounted as the E: drive. On the mounted drive navigate to sources\sxs


Make a zip file of the Sxs folder (Sxs.zip) and copy the zip file to the Windows Server 2012 R2 host requiring .NET Framework 3.5. Unzip the file on the Windows Server 2012 R2 host (see C:\Source\Sxs below):


On the Windows Server 2012 R2 run PowerShell as an administrator and from the PowerShell console invoke the Install-WindowsFeature cmdlet as follows:

Install-WindowsFeature Net-Framework-Core -source C:\share\sxs

As is show below by the Add Roles and Features Wizard, .NET Framework 3.5 is now installed:


Appendix A: Installing .NET Framework 3.5 with Server Manager (ending in failure)

The traditional way to install a feature such as .NET Framework 3.5 is by running Server Manager and selecting Add roles and features. This section attempts to demonstrate installing .NET Framework 3.5 in this fashion but ultimately the installation will end in failure. 

The steps to unsuccessfully install .NET Framework 3.5 on Windows Server 2012 R2 are as follows. 

Run Server Manager: 


From Dashboard select Add roles and features which starts the Add Roles and Features Wizard:


Click Next:


By default the radio button Role-based or feature-based installation is checked. Leave this radio button checked and click on Next:


By default the current server will be selected and the radio button Select a server from the the server pool will be checked. Leave the default settings and click the Next button:


Click the Next button:


Click on the checkbox labeled .NET Framework (includes .NET 2.0 and 3.0). This action will active the Next button as follows:


Click on Next:


The failure message provides the clue as to how to install .NET 3.5. The full text of this failure message is: Do you need to specify an alternate source path? One or more installation selections are missing source files on the destination server. The server will try to get missing source files from Windows Update, or from a location that is specified by Group Policy. You can also click the “Specify an alternate source path” link on this page to provide a valid location for the source files.



















Friday, July 3, 2020

Installing SharePoint 2013 SP1 on Windows 2012 R2 (working around the .NET 4.5 restriction)

There was a purpose to the previous post, PowerShell: Determining the versions of .NET Installed on Windows. I have been tasked to install SharePoint 2013 SP1 on a Windows Server 2012 R2 machine. Unfortunately SharePoint 2013 SP1 has a well-known issue, it will not install if a version of .NET is installed on the machine that is greater than version 4.5.x. The error message generated by the SharePoint 2013 SP1 is as follows, This product requires Microsoft .NET Framework 4.5:



The work-around is to rollback the .NET Framework on the server on which SharePoint 2013 SP 1 will be installed. This is described below because as of July 3, 2020 it appears that the only way to install SharePoint 2013 SP1 on a machine where the version of .NET Framework is not greater than 4.5.x. The solution shown below is actually a post and a series of replies from (techcommunity.microsoft.com) Need "svrsetup_15-0-4709-1000_x64.zip" for SharePoint Server 2013 (KB3087184). The final reply is my solution to the issue.

Microsoft has documented the "greater than .NET Framework 4.5 error" and has proposed solutions such as Setup error if the .NET Framework 4.6 is installed. In the aforementioned article Microsoft points administrators to a magic Knowledge Base article (KB2880552), Service Pack 1 for Microsoft SharePoint Server 2013 (KB2880552). From this magic Knowledge Base article is a magic link to magic page where a magic zip file can be downloaded. This magic zip file contains a magic DLL that can be added to the SharePoint 2013 SP1 files extracted from the installation media. Depending on the SharePoint product being installed the DLL's supposedly available are as follows:
  • SharePoint Foundation 2013 with Service Pack 1: wsssetup.dll
  • SharePoint Server 2013 with Service Pack 1: svrsetup.dll
  • Project Server 2013 with Service Pack 1: svrsetup.dll
The link to KB2880552 no longer contains a zip file and there there are no magic DLLs that allow SharePoint 2013 to be installed on a machine containing .NET Framework 4.6.x, 4.7.x or 4.8.x.

Shown at the very bottom of this post is the solution demonstrating the steps to rollback to .NET Framework 4.5.x, install SharePoint 2013 SP1, and restore the previous .NET Framework or .NET Frameworks that had been installed.