TFS Scripts

I’m definitely not a TFS genius, but I’ve written a few scripts that have proven helpful in dealing with some of the issues that come up with version control.
First, here’s a simple one. This just automates a simple TF.EXE command to show the last 50 check-ins in our project. This particular command opens a GUI window to show the output.

# https://gist.github.com/andyhuey/5471064
[string]$tf = "C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\TF.exe"
pushd
cd c:\ax2012tfs
& $tf history /r /stopafter:50 *
popd

Second, here’s one to show the TFS status. This command, unlike the previous, sends output to the console, so I’m piping it to Notepad++, so I can see it there.

# https://gist.github.com/andyhuey/5471072
[string]$tf = "C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\TF.exe"
[string]$npp = "C:\Program Files (x86)\Notepad++\notepad++.exe"
# [string]$tempFile = [System.IO.Path]::GetTempFileName()
[string]$tempFile = "$env:temp\tfStatus.txt"
pushd
cd c:\ax2012tfs
& $tf status > $tempFile
popd
& $npp $tempFile

And third, here’s a somewhat more complicated one. This one allows you to diff two changesets, and pipes the output to Notepad++. But, if there’s an error, it instead shows a “press any key” message, so you can see the error in the console window. Notepad++ has syntax highlighting for diff files, so the output is reasonably nice-looking.

# https://gist.github.com/andyhuey/5471084
param (
     [string]$cs1 = $( Read-Host "Enter changeset 1 (as c9999)" ),
     [string]$cs2 = $( Read-Host "Enter changeset 2 (as c9999)" )
)
[string]$tf = "C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\TF.exe"
[string]$npp = "C:\Program Files (x86)\Notepad++\notepad++.exe"
[string]$tempFile = "$env:temp\tfDiff.diff"
pushd
cd c:\ax2012tfs
& $tf diff cus /v:$cs1~$cs2 /r /f:unified > $tempFile
if ($LastExitCode -eq 0)
{
     & $npp $tempFile
}
else
{
     Write-Host "Press any key to continue ..."
     $x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}
popd

This pretty much concludes the overview of my utility scripts that I started a few days ago. I hope it was helpful to someone. If not, at least I’ve got them documented now, so if I lose them again, I know where to look!

Exporting Projects From AX

After my hard drive crash, I decided that I should really create a way to automatically back up all the code for all of my active projects. So, I’ve added an “exportAllProjects()” method to my “startup projects” class. This method takes my list of active projects, iterates through them, and exports each one to a standard XPO file, in a sub-folder off the My Documents folder.

I’ve created a menu item for this, and attached it to the Development Tools menu. So, now I can backup all the objects in all of my active projects in one fell swoop. It would nice to be able to do this automatically, but I’m not sure I want to start messing around with doing this as a scheduled job just yet.

// https://gist.github.com/andyhuey/5470950
public void exportAllProjects()
{
    // get the project list, and export all projects.
    Array projects;
    int i;
    ProjectNode sharedProjects, privateProjects, projectNode;
    str myDocsPath, filePath;

    #WinAPI

    myDocsPath = WinAPI::getFolderPath(#CSIDL_PERSONAL);
    myDocsPath += @"\xpo_backup";

    // make sure myDocsPath exists.
    if (!WinAPI::pathExists(myDocsPath))
    {
        WinAPI::createDirectoryPath(myDocsPath);
    }

    projects = this.getProjectList();

    sharedProjects = Infolog.projectRootNode().AOTfindChild('Shared');
    if (!sharedProjects)
        throw error("Error: cannot located shared project node!" );

    privateProjects = Infolog.projectRootNode().AOTfindChild('Private');
    if (!privateProjects)
        throw error("Error: cannot located private project node!" );

    for (i= 1; i <= projects.lastIndex(); i++)
    {
        // skip any line starting with a semi-colon
        if (subStr (projects.value(i), 1, 1) == ";" )
            continue;

        projectNode = sharedProjects.AOTfindChild(projects.value(i));
        if (!ProjectNode)
            projectNode = privateProjects.AOTfindChild(projects.value(i));

        if (projectNode)
        {
            filePath = myDocsPath + @"\" + projects.value(i) + ".xpo" ;
            projectNode.treeNodeExport(filePath);
        }
        else
            warning(strFmt("Project %1 cannot be found." , projects.value(i)));
    }
}

Remapping Keystrokes in MorphX

The shortcut keys used in MorphX, the Dynamics AX code editor, are almost exactly the same as those used in Visual Studio. In fact, the code editor basically is the editor from Visual Studio, with somewhat reduced functionality, if I understand correctly.

The one thing that’s always bugged me about it is that the keystrokes for commenting and un-commenting code are different. In VS (and various other editors), it’s Ctrl-K,Ctrl-C and Ctrl-K,Ctrl-U. For no obvious reason, MorphX uses Ctrl-E,Ctrl-C and Ctrl-E,Ctrl-U. This isn’t too bad, until you start getting used to it, then you accidentally press Ctrl-E in SQL Management Studio, hence executing a block of SQL instead of commenting it out. After doing that a few times, I decided that I needed to fix MorphX.

Surprisingly, I couldn’t find any facility built into AX for changing keyboard shortcuts. So, I turned to AutoHotKey. It’s very easy to remap a single keystroke in AHK. For instance, I can just remap Ctrl-K to Ctrl-E with “^k::^e”. I went ahead and did that for awhile, since it didn’t really seem that there would be any harm in that. But, I wanted to figure out how to create a more targeted replacement, so only the two specific commands would get remapped.

The snippet below does that. And, or course, it limits the remapping to the AX code editor.

; https://gist.github.com/andyhuey/5466566
; comment/uncomment, the way it was intended to be...
#IfWinActive ahk_class AxMainFrame
^k::
Transform, CtrlC, Chr, 3
Transform, CtrlU, Chr, 21
Input Key, L1 M T1
if Key = %CtrlC%
     Send ^e^c
if Key = %CtrlU%
     Send ^e^u
return
#IfWinActive

Backup Script

In my last post, I mentioned that I was going to write up some of the utility scripts I have on my VM. The first one is pretty simple. It’s a little PowerShell script to zip up the My Documents folder on the VM, and copy it to the physical machine. (I’m using 7-Zip.)

There are a few things in this script that are pretty common tasks that I need to do when using PowerShell, so this is a good thing to put up on the blog for reference. Just to point out those things:

  1. Creating a file name that contains the current date.
  2. Running a command that’s in a string variable.
  3. Prompting to “press any key” when done, so the user can see error messages, if the script is being run from a desktop icon.
  4. Giving an option to skip the “press any key” prompt, when the command is run unattended from task scheduler.
# https://gist.github.com/andyhuey/5466524
param(
     [switch]$quiet
)
$zipExe = "C:\Program Files\7-Zip\7z.exe"
$dateStr = '{0:yyyy-MM-dd}' -f (Get-Date)
$buFileName = "\\my-machine\c$\Users\me\Documents\backup\VM_MyDocBU_" + $dateStr + ".7z"
$myDocs = "C:\Users\me\Documents"
pushd
cd $myDocs
& $zipExe a -r $buFileName $myDocs
popd
if (!$quiet)
{
     Write-Host "Press any key to continue ..."
     $x = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
}

Hard drive crash

One of the hard drives on my work PC crashed a couple of days ago. My work PC is (or rather, was) configured with an SSD for a boot drive, and two regular SATA drives, in a RAID 0 configuration, for a secondary data volume. It was one of those SATA drives that failed. Since RAID 0 doesn’t have any redundancy built in, that killed the volume.

The only data I had on that volume were the files for my VM. The way we have developer machines configured here, we have general productivity stuff (Office, etc) on the boot volume, and all the developer stuff on the VM. The setup for developing for Dynamics AX is fairly complicated, so it makes sense to do it on a VM.
Unfortunately, we don’t have any facility set up for backing up our VMs anywhere. Also, between the way AX stores source files, and the way we have TFS set up, we don’t always check in code daily, nor do we have a simple way of backing up in-progress code changes that haven’t been checked in. So, the end result is that I lost about two days worth of work on my current project.
I had, at one point, written a backup script (using PowerShell and 7-Zip) to back up the My Docs folder on the VM to the My Docs folder on the physical machine, but I hadn’t ever set it to run on a scheduled basis, so the backup file there was about a week old, which meant that I also lost a few SQL files, some test spreadsheets, and one quickie VS 2010 project that I’d written to test a web service. Oh, and I was keeping the backup script itself (plus some other scripts) in a ‘util’ folder on the root of the VM C: drive, so those didn’t get backed up either, and were lost.
So the takeaway from all of this, of course, is that I need to do what I can to get around the limitations of the environment I’m working in, and set up some automated backup procedures.
In terms of backing up the My Docs folder, I rewrote my lost PowerShell script, and set it up in task scheduler to run at 6pm daily. It ran fine last night, and I think it’ll work fine on a continuing basis.
In terms of backing up in-progress work in AX, I extended the ‘startup projects’ class that I blogged about recently to also allow me to export all of my active projects. I have it exporting them to a folder under the My Docs folder, so, if I run the export at the end of the day, prior to the file system backup, I should always have a backup of my current work, in a format that I can re-import into AX, if need be.
There are still some big holes in this system, including the fact that I have to remember to run that export daily. But it’s a good start. I’d like to add some extra stuff to this process, including daily SQL backups, and maybe a push of certain backup files to the cloud. The SQL backups are kind of hard, since the AX test database is 70 GB. And my employer, for some reason, likes to block access to cloud-based backup & storage providers, so I can’t just copy stuff into a DropBox folder, so that part’s a little tricky too. 
I’ve also considered setting up a local Mercurial or Git repo, checking in the AX export files every day, and pushing them up to a private Bitbucket repo. This would give me offsite backup, with the added benefit of increased granularity and visibility, but it would probably violate one or more corporate policies.
As a follow-up to this post, I’m going to write a few more posts, about some of the scripts I’m using now.

Opening multiple projects in MorphX at startup

The MorphX IDE used for X++ programming in Microsoft Dynamics AX is a fairly decent environment to work in, but it definitely has some shortcomings and oddities. There is a “project” abstraction in MorphX that allows you to create a named group of objects that all relate to a project that you are working on. There’s little meaning to these projects, other than that. You can export all objects in a project into a single XPO file, but other than that, it’s basically just a structure to help a programmer keep track of a list of related objects. You can set a single project as your “startup project”, and that project will then open automatically when you open MorphX. Since I’m usually juggling three or four projects at a time, I’ve been thinking that it would be great if you could open a group of projects instead of just one, so I decided to try and write some code to do that.

My realization that this could be done at all mostly came from the book Microsoft Dynamics AX 2012 Development Cookbook. The section “Modifying the right-click context menu” described a method of setting and clearing the startup project via the context menu. Prior to reading this, I hadn’t realized that the development environment was as customizable as the AX front-end, though (in retrospect) it makes sense that it is.

The project I’m going to describe here will do a few, fairly simple, things:

  1. Maintain a list of startup projects in a simple text file, stored in my personal “documents” folder.
  2. Allow for adding and removing projects from this list, via the right-click context menu.
  3. Allow the user to open all projects in this list, via the MorphX “tools” menu.

I could probably do a lot more with this project, such as actually open the projects at MorphX startup, rather than just through the tools menu, but I’m fine with the project as-is, for now.

This blog post is going to walk through the steps necessary to implement this functionality.

First, we’re going to create a class called “AjhDevLaunchStartupProjects”. Our class will have methods to add and remove projects from the list, and to open all projects specified in the list. We’re not going to go overboard with efficiency here. I’m assuming that the list will, at most, have three or four projects on it at a time, so we’re simply going to read them from disk into an array, and write them back out from the array.

We’ll start with an entirely standard static constructor:

// https://gist.github.com/andyhuey/5315492
public static AjhDevLaunchStartupProjects construct()
{
    return new AjhDevLaunchStartupProjects();
}

Now let’s add a method to get the file name for the text file:

// https://gist.github.com/andyhuey/5315509
private str getStartProjFileName()
{
    str myDocsPath;

    #WinAPI

    myDocsPath = WinAPI::getFolderPath(#CSIDL_PERSONAL);
    return myDocsPath + @"\axStartProjects.txt";
}

Now, methods to read and write the file:

// https://gist.github.com/andyhuey/5315524
private Array getProjectList()
{
    // get the project list from a file.
    str startProjFileName;
    TextBuffer tbProjList;
    Array projects = new Array(Types::String);
    int nProjects;

    startProjFileName = this.getStartProjFileName();
    tbProjList = new TextBuffer();

    // if it doesn't exist, create an empty file & return.
    if (!WinAPI::fileExists(startProjFileName))
    {
        WinAPI::createFile(startProjFileName);
        return projects;
    }

    // should probably assert permission...
    tbProjList.fromFile(startProjFileName);

    nProjects = 0;
    while (tbProjList.nextToken(true))
    {
        nProjects++;
        projects.value(nProjects, tbProjList.token());
    }
    return projects;
}

private boolean writeProjectList(Array projects)
{
    // write out a project list to file, overwriting existing list.
    str startProjFileName;
    TextBuffer tbProjList;
    int i;

    startProjFileName = this.getStartProjFileName();

    tbProjList = new TextBuffer();

    for (i=1; i <= projects.lastIndex(); i++)
    {
        tbProjList.appendText(projects.value(i));
        tbProjList.appendText("\n");
    }

    return tbProjList.toFile(startProjFileName);
}

And the methods to add and remove projects from the list:

// https://gist.github.com/andyhuey/5315547
private void addProject(str newProject)
{
    // add a new project to the list.
    Array projects;
    int i;

    projects = this.getProjectList();

    // make sure it's not already there...
    for (i=1; i <= projects.lastIndex(); i++)
    {
        if (projects.value(i) == newProject)
            return;
    }
    // add it and save.
    projects.value(projects.lastIndex()+1, newProject);
    this.writeProjectList(projects);
}

private void removeProject(str projectToRemove)
{
    // remove a project from the list.
    Array projectsIn, projectsOut;
    int i, j;

    projectsIn = this.getProjectList();
    projectsOut = new Array(Types::String);

    j=1;
    for (i=1; i <= projectsIn.lastIndex(); i++)
    {
        if (projectsIn.value(i) != projectToRemove)
        {
            projectsOut.value(j, projectsIn.value(i));
            j++;
        }
    }
    this.writeProjectList(projectsOut);
}

And here’s the code to open all active projects:

// https://gist.github.com/andyhuey/5315552
public void openAllProjects()
{
    // get the project list, and open all projects.
    Array projects;
    int i;
    ProjectNode sharedProjects, privateProjects, projectNode;

    projects = this.getProjectList();

    sharedProjects = Infolog.projectRootNode().AOTfindChild('Shared');
    if (!sharedProjects)
        throw error("Error: cannot locate shared project node!");

    privateProjects = Infolog.projectRootNode().AOTfindChild('Private');
    if (!privateProjects)
        throw error("Error: cannot locate private project node!");

    for (i=1; i <= projects.lastIndex(); i++)
    {
        projectNode = sharedProjects.AOTfindChild(projects.value(i));
        if (!ProjectNode)
            projectNode = privateProjects.AOTfindChild(projects.value(i));

        if (projectNode)
            projectNode.getRunNode();
        else
            warning(strFmt("Project %1 cannot be found.", projects.value(i)));
    }
}

(I probably found the code to open a project from this blog post, or a similar one.)
Now, we will create three new “action” menu items:

  1. AjhDevStartupProjectAdd – to add a project.
  2. AjhDevStartupProjectRemove – to remove a project.
  3. AjhDevStartupProjectOpenAll – to open all projects.

Each one will have ObjectType set to “Class”, and the object will be our class, “AjhDevLaunchStartupProjects”. The static main() method in our class will use args.menuItemName() to determine which action to take.
Here’s that method:

// https://gist.github.com/andyhuey/5315559
public static void main(Args args)
{
    AjhDevLaunchStartupProjects obj = AjhDevLaunchStartupProjects::construct();
    SysContextMenu contextMenu;
    str projectName, x;
    ;

    // should always be called from a menu item.
    if (!args.menuItemName())
    {
        return;
    }

    // if called from the add-ins context menu...
    if (SysContextMenu::startedFrom(args))
    {
        contextMenu = args.parmObject();
        projectName = contextMenu.getFirstNode().treeNodeName();
    }

    switch (args.menuItemName())
    {
        case menuitemActionStr(AjhDevStartupProjectAdd):
            obj.addProject(projectName);
            break;
        case menuitemActionStr(AjhDevStartupProjectRemove):
            obj.removeProject(projectName);
            break;
        case menuitemActionStr(AjhDevStartupProjectOpenAll):
            obj.openAllProjects();
        default:
            return;
    }
}

The add and remove menu items will be added to the SysContextMenu. This way, they will show in the context menu under “add-ins”. The “open all” menu item will be added to the DevelopmentTools menu. This way, it will show under the “Tools” menu in MorphX.

We will also change the “verifyItem” method of the “SysContextMenu” class, so that the add & remove items will only show in the context menu for projects (and not for other objects). (If we wanted to go further with this, we would also add logic here to show only one or the other option, depending on whether or not the project is already on the startup list.)

Here’s the code that we will add to “verifyItem”, at the end of the large case statement there:

    // https://gist.github.com/andyhuey/5315566
    // ajh 2013-04-03: my own startup project thing...
    case menuitemActionStr(AjhDevStartupProjectAdd):
    case menuitemActionStr(AjhDevStartupProjectRemove):
        if (firstNode.handle() != classNum(ProjectNode) || !match(#pathProjects, firstNode.treeNodePath()))
        {
            return 0;
        }
        return 1;

So I think this is all pretty straightforward. My purpose in writing this up in such detail was largely so that I could get a bit of X++ code on this blog, and in the hope that someone else might find this useful. I haven’t had the chance to write much general-purpose code at my current job, so there isn’t much I’m working on that would be appropriate to post here, but this seemed like a worthwhile little project to work on, and hopefully it may prove helpful to someone else.