Scott Hanselman

ScriptBlock and Runspace Remoting in PowerShell

July 15, '06 Comments [2] Posted in PowerShell | XmlSerializer | Web Services | Bugs
Sponsored By

When I first saw grokked PowerShell I wanted to be able to issue commands remotely. There's some great stuff going on with PowerShellRemoting over at GotDotNet, but that's remoting of the User Interface.

I want to be able to issue commands to many different machines in a distributed fashion.

After some pair programming with Brian Windheim, we set up a Windows Service that would get a string of commands and return a string that was the output o those commands. I could then issue remote commands, but the result at the client was just strings. I was in PowerShell but I'd just made the equivalent of PSEXEC for PowerShell...so basically I'd gotten nowhere.

Ideally I'd like to have behavior like this (but I don't):

using (Runspace myRunSpace = RunspaceFactory.CreateRunspace("COMPUTERNAME"))

{

    myRunSpace.Open();
}

But a Runspace is local and inproc. I don't see a really obvious and straightforward way to do this, considering that there's LOTS of internal and private stuff going on within PowerShell.

I liked that the string in, string out remoting stuff worked fine, but really I want to get Objects back from the remote machine. So, I started using Reflection to poke around inside System.Management.Automation.Serializer, but that got evil quickly. Truly evil.

Then I had an epiphany and remember the Export-CliXml cmdlet. It is the public cmdlet that uses the serializer I was trying to get to. It isn't the XmlSerializer. It's a serialized graph of objects with a rich enough description of those objects that the client doesn't necessarily need the CLR types. If reflection had a serialization format, it might look like this format.

Now, if I take the commands I was issuing to the remote "invoker" and export the result of the pipeline to this function XML format, I've just discovered my remoting server's wire format.

This RunspaceInvoker type is hosted in a Windows Service, but it could be in any Remoting hosting process. I'll likely move it inside IIS for security reasons. The app.config for my service looks like this:

<?xml version="1.0"  encoding="utf-8" ?>

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">

  <system.runtime.remoting>

    <customErrors mode="Off"/>

    <application>

      <channels>

        <channel ref="http" port="8081"/>

      </channels>

      <service>

        <wellknown mode="SingleCall"

                   type="Hanselman.RemoteRunspace.RunspaceInvoker,
                   Hanselman.RemoteRunspace
" objectUri="remoterunspace.rem"/>

      </service>

    </application>

  </system.runtime.remoting>

</configuration>

Note the objectUri and port. We'll need those in a second. There's an installer class that I run using installutil.exe. I set the identity of the Windows Service and it starts up with net start RemoteRunspaceService.

This is the RunspaceInvoker (not the best name):

 public class RunspaceInvoker : MarshalByRefObject

 {

    public RunspaceInvoker(){}

 

    public string InvokeScriptBlock(string scriptString)

    {

        using (Runspace myRunSpace = RunspaceFactory.CreateRunspace())

        {

            myRunSpace.Open();

 

            string tempFileName = System.IO.Path.GetTempFileName();

            string newCommand = scriptString +
                " | export-clixml " + "\"" + tempFileName + "\"";

            Pipeline cmd = myRunSpace.CreatePipeline(newCommand);

 

            Collection<PSObject> objectRetVal = cmd.Invoke();

 

            myRunSpace.Close();

 

            string retVal = System.IO.File.ReadAllText(tempFileName);

            System.IO.File.Delete(tempFileName);

            return retVal;

        }

    }

 }

A command for the remote service comes into the scriptString parameter. For example we might pass in dir c:\temp as the string, or a whole long pipeline. We create a Runspace, open it and append "| export-clixml" and put the results in a tempFileName.

THOUGHT: It's a bummer I can't put the results in a variable or get it out of the Pipeline, but I think I understand why they force me to write the CLI-XML to a file. They are smuggling the information out of the system. It's the Heisenberg Uncertainly Principle of PowerShell. If you observe something, you change it. Writing the results to a file is a trapdoor that doesn't affect the output of the pipeline. I could be wrong though.

Anyway, this doesn't need to be performant. I write it to a temp file, read the file in and delete it right away away. Then I return the serialized CLI-XML to the caller.The client portion is two parts. I probably should make a custom cmdlet, but I didn't really see a need. Perhaps someone can offer me a reason why.

For simplicity I first made this RunspaceProxy. Remember, this is the class that the client uses to invoke the command remotely.

    public class RunspaceProxy

    {

        public RunspaceProxy()

        {

            HttpChannel chan = new HttpChannel();

            if (ChannelServices.GetChannel("http") != null)

            {

                ChannelServices.RegisterChannel(chan, false);

            }

        }

 

        public Collection<PSObject> Execute(string command, string remoteurl)

        {

            RunspaceInvoker proxy = (RunspaceInvoker)Activator.GetObject(
                   typeof(RunspaceInvoker), remoteurl);

            string stringRetVal = proxy.InvokeScriptBlock(command);

 

            using (Runspace myRunSpace = RunspaceFactory.CreateRunspace())

            {

                myRunSpace.Open();

                string tempFileName = System.IO.Path.GetTempFileName();

                System.IO.File.WriteAllText(tempFileName, stringRetVal);

                Pipeline cmd = myRunSpace.CreatePipeline(
                    "import-clixml " + "\"" + tempFileName + "\"");

                Collection<PSObject> retVal = cmd.Invoke();

                System.IO.File.Delete(tempFileName);

                return retVal;

            }

        }

    }

I'm using the HTTP channel for debugging and ease of use with TcpTrace. The command to be executed comes in along with the remoteUrl. We make a RunspaceInvoker (the class we talked about a second ago) on the remote machine and it does the work via a call to InvokeScriptBlock. The CLI-XML comes back over the wire and now I have to make a tempfile on the client. Then, in order to 'deserialize' - a better word would be re-hydrate - the Collection of PSObjects, I make a local Runspace and call import-clixml and poof, a Collection<PSObject> is returned to the client. I delete the file immediately.

Why is returning real PSObjects so important when I had strings working? Because now I can select, sort, and where my way around these PSObjects as if they were local - because they are. They are real and substantial. This will allow us to write scripts that blur the line between the local admin and remote admin.

Now, all this has been C# so far, when does the PowerShell come in? Also, since I've worked so hard (well, not that hard) to get the return values integrated cleanly with PowerShell, what's a good way to get the remote calling of scripts integrated cleanly?

My first try I made a function RemoteInvoke() that took a command string. It worked, but felt tacky. Then I remembered how Jeffrey Snover said to look to Type Extensions when adding functionality rather than functions and cmdlets.

I made a My.Types.ps1xml file in my PSConfiguration directory and put this in it:

<Types>

  <Type>

    <Name>System.Management.Automation.ScriptBlock</Name>

    <Members>

      <ScriptMethod>

        <Name>RemoteInvoke</Name>

        <Script>

          if ($GLOBAL:remoteUrl -eq $null) { throw 'Set $GLOBAL:remoteUrl first!' }


          [System.reflection.assembly]::LoadWithPartialName("System.Runtime.Remoting") |
               out-null

          $someDll = "C:\foo\Hanselman.RemoteRunspace.dll"

          $asm = [System.Reflection.Assembly]::LoadFrom($someDll) | out-null


          $runspace = new-object Hanselman.RemoteRunspace.RunspaceProxy


          $runspace.Execute([string]$this, $GLOBAL:remoteUrl);

        </Script>

      </ScriptMethod>

    </Members>

  </Type>

</Types>

Then called Update-TypeData My.Types.ps1xml (actually it's in my profile so it happens automatically.)  This file adds a new method to the ScriptBlock type. A ScriptBlock is literally a block of script. It's a very natural "atom" for us to use.

NOTE: I'd like to have the RemoteUrl be a parameter to the RemoteInvoke ScriptMethod, but I can't fine really any documentation on this. I'll update it when I figure it out, but for now it uses a $GLOBAL variable and freaks out if it's not set.

The RemoteInvoke loads the .NET System.Runtime.Remoting assembly, then it loads our Proxy assembly. Then it calls Execute, casting the [ScriptBlock] to a [string] because the Runspace takes a string.

For example, at a PowerShell prompt if I do this:

PS[79] C:\> $remoteUrl="http://remotecomputer:8081/RemoteRunspace.rem"

PS[80] C:\PS[80] C:\> 2+2
4

PS[81] C:\> (2+2).GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int32                                    System.ValueType

PS[82] C:\> {2+2}.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    ScriptBlock                              System.Object

PS[83] C:\> {2+2}
4

PS[84] C:\> {2+2}.RemoteInvoke()
4

PS[85] C:\>
{2+2}.RemoteInvoke().GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Int32                                    System.ValueType

Note the result of the last line. The value that comes out of RemoteInvoke is an Int32, not a string. The result of that ScriptBlock executing is a PowerShell type that I can work with elsewhere in my local script.

Here's the CLI-XML that went over the wire (just to make it clear it's not XmlSerializer XML):

<Objs Version="1.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">

  <I32>4</I32>

</Objs>

This 2+2 stuff is a terse and simple example, but this technique works with even large and complex object graphs like the FileInfos and FileSystemInfo objects that are returned from dir (get-childitem).

Remoterunspace

In this screenshot we do a get-process on the remote machine then sort and filter the results just as we would/could if the call were local.

My WishList for the Next Version of PowerShell

  • All this stuff I did, built in already with security and wonderfulness.
  • All the stuff in PowerShellRemoting, with security and wonderfulness.
  • Some kind of editor or schema installed in VS.NET for editing My.Types.ps1xml.
  • TabExpansion for all Types in the current AppDomain (this of course, already done by MonadBlog and MOW).

Thanks again to Brian Windheim for the peer programming today that jump started this!

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Do I sleep?

July 14, '06 Comments [10] Posted in Musings
Sponsored By

Strangely today two separate (and presumably unrelated) bloggers questioned whether or not I sleep. David Suruyange posted on it and Ryan Rinaldi did also, albeit in passing.

However, I must say, I do have quite a fondness for Nancy Kress' Beggars in Spain (Wikipedia).

"The book deals with the issues of genetic modification to unborn babies and problems that may arrive within society from such technologies. Specifically, the book examines a genetic modification that renders the babies capable of never having to sleep. This allows the babies, as they grow, the chance to accomplish much more with their freed up time as well as a collection of secondary genetic traits. The books charts the reactions of society to the 'Sleepless' from the viewpoint of the 'Sleepless' group as they struggle to find a place in society and battle against the prejudices they face.

The book's protagonist, Leisha Camden, is a sleepless person who comes from an extremely rich family and whose life is paralleled and compared to that of her twin who is born without the genetic modification for sleeplessness."

One sure could get a lot done if they didn't sleep. Do you sleep?


In other news, Scott Bellware (my personal Evil Spock) has proceeded to register Hanselmetric.com and is (very likely) proceding to build a (top secret) Froogle search populated entirely by things I've got in my house. Here is his exceedingly thoughful business strategy:

"What the world needs now is love sweet love, and a website that aggregates the Hanselman product testimonials scattered around all the various websites so that we can plan our future purchases accordingly.  Googling "scott hanselman testimonial" simply isn't accurate enough when arriving up the end of the month with a couple extra bucks in my pocket and a burning desire to spend .  Dammit, I want to know what other products satisfy the Hanselmetric that I haven't been made aware of, and until I do, I just feel sort of empty inside.  The Hanselmetric represents the pinnacle of an aspect of contemporary cultural evolution, and I just don't want to be left behind."

Madness! Hm...maybe we can make some kind of cooperative crossover affiliate work. Let's focus first on getting me on Oprah. Or at least some kind of local cable network or QVC. Give me a call ScottB! ;)

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

A new day, two new browsers compared - Firefox 2.0 Beta 1 and IE 7.0 Beta 3

July 14, '06 Comments [18] Posted in XML | HttpHandler | Bugs | Tools
Sponsored By

Like a schmuck, I can't resist installing beta software, especially REALLY beta software, especially on computers that I really shouldn't be installing beta software on like my computer or my wife's, especially at 2am. After some painful installation chaos/debugging...I got Firefox 2.0 Beta 1 and IE 7.0 Beta 3 installed. Here's my thoughts.

First Impressions:

Firefox 2.0 Beta 1: Still ugly as hell by default. For crying out loud, just make up with the Qute guy and set his theme by default. Sheesh.

IE 7: Different, softer, minimalist. I like the tabs. It's odd, but feels familiar.

RSS

Recall that IE7 installs the new Windows RSS Platform with it's Common Feed List that 90% of Windows Aggregators will start using for storage. Since Firefox doesn't have a Common Feed Store and doesn't recognize the new Windows Common Feed Store (until someone writes a utility to register feed:// with the Common Feed List) it displays a list of registered feed readers on your system. I really like this approach as it works with my FeedDemon addiction, and will work with not only offline aggregators like RSSBandit and SharpReader, but online ones like Bloglines and MyYahoo.

Both browsers show a very nice, styled page rather than the underlying RSS/XML. IE uses heuristics to figure out if it's a feed and also handles lots of malform-edness, while AFAIK Firefox goes by mime/type.

IerssFirefoxrss

In IE, since it includes a feed retrieval engine, there's a number of very nicely designed dialogs for updating the schedules for each feed, as well as their attached (enclosed) files.

Ierss2

Anti-Phishing

The need for a browser to have Anti-Phishing capabilities is huge in my opinion. Comparing Firefox 2.0 Beta 1 with IE 7.0 Beta 3, I visited six brand new, known, phishing sites. (Remember, Corillian knows Anti-Phishing)

Interestingly, and surprisingly to me, only IE detected every single phishing site. Firefox didn't detect a single one. I would point out also, as an aside, that Outlook 2007 went to great lengths to keep me from visiting these sites.

Perhaps the phishing services that Firefox uses at Google were down during my test? When Firefox does detect a phishing site it's supposed to look like this, below. I was hoping to have two identical phishing sites side by side, but again, Firefox didn't detect anything suspicious on these sites, so I visited the Firefox test phishing site. You can also submit naughty sites to Google.

IE7 showing a suspicious websiteFirefox showing a suspicious website

One interesting note, both browsers allowed me to continue working with the known phishing site, even though they knew they were dangerous. I think I'd rather have a dialog that made me type in "I know this is a phishing site."

Usability

IE7 has a new "tile" feature available by default; it's the little 4-square button to the far left of the tabs. It's "exposé" for web pages. There's a number of similar Firefox extensions like Foxpose and Tabexpose, but none for Firefox 2.0 today. Give it a few hours. Interesting that IE7 includes it by default, though, with Ctrl-Q (QuickTabs) as the hotkey.

The little "star" non-committal icon aims to say "click me, it'll make sense soon." This opens a the Favorites|Feeds|History pane. Eh.

IE7 with four sites open, tiledIE7 with History Pane open

In IE7, I love that CTRL-Scroll is how mapped to the new (and WAY more useful) zoom feature (ala Opera 1997), although I was disappointed to see that the useless Text Size Feature is next to it. Breeds confusion I say. Screw you, Text Size and your lies.

Iezoom

There's dozens of other usability things in IE7 that are obvious, including the revamped Tools|Options for Humans, which I wouldn't be afraid to send my Dad into. Except for Tools|Options|Advanced - that's still scary even to me.

In Firefox 2.0, less has changed, at least, less dramatic stuff. The coolest new feature, IMHO, is the inline spell checker.

Firefoxspellcheck

There's another new feature called micro-summaries. Basically it's your page's opportunity to change the title of a bookmark to provide status. For example, Woot provides a micro-summary by specifying this in their HTML:

 <link rel="microsummary" type="application/x.microsummary+xml" href="DefaultMicrosummary.ashx" />

That HttpHandler might responsd with just this: "$69.99 : Nexxtech 7” 16:9 LCD Portable DVD Player" as text/plain. That text could then be used as the title of a bookmark or toolbar button. It's a little early, and very specilized, but it fills a small gap and adds on to the whole link rel="" vibe nicely. Could really take off ono mobile devices as the equivalent of "tiny RSS."

Firefox also includes the trappings of client-side session support that could take AJAX apps to the next level in the next few years if Microsoft jumps on board also.

Downloads

The download windows of each browser look pretty much the same.

Iedownload

There's a new animation in IE's, but otherwise, not much here. I still prefer Firefox's consolidated view.

Firefoxdownload

Conclusion

Before I was pretty much a one browser guy, Firefox. Now I think I'll try IE more, and even more likely I'll notice which browser I'm in less as they are converging. The RSS features of IE7 are compelling and as soon as there's a NewsGator Online/RSS Common Feed Store synchronization story along with FeedDemon support, I might spend more time in IE7.

Most importantly I won't feel back recommending either of these browsers to family. Both are WAY safer than their predecessors. (Assuming the future parity of their phishing feature)

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Some tips for saving money

July 14, '06 Comments [9] Posted in Musings
Sponsored By

I met with some family friends today, at their request, to do a simple financial and debt analysis and come up with some ideas on how they could save money month to month.

Here's the list they left with:

  • Don't get tricked by Irregular Pay Periods - If one spouse is paid "every other Weds" and one is paid on the 1st and the 15th, pick one schedule to go with. For example, take the every other Weds paycheck and deposit it into a separate account and "pay yourself" on the 1st and the 15th.
  • Be aware of Irregular Bills - Have an interest bearing account where you pay a "simulated monthly bill" (much like a mortgage escrow account behaves) that builds up until the yearly/biyearly payment comes out.
  • Turn off everything in the house (as if you were on a trip) - When you go out for a long trip you likely prepare the house by turning things off. Why not do this all the time and work out a system where minimal things are on while you're at work? LCD or not, your computer monitor is a big light bulb. Forget screen savers, just turn it off.
  • Save for Property Taxes Monthly - In Oregon, Yearly Property Taxes tend to sneak up on folks. Save for those taxes, and everything irregular, in a regular way like your paycheck. Money in, money out, same every month, makes for a predictable lifestyle.
  • Call the Electric Company – Some electric companies give out Coupons for High Efficiency Lighting (fluorescent). Call yours.
  • Library Book/Videos Box near the Front Door - Don't pay late fees. Leave a shoebox near the door you leave from. Make it a habit to put things like library books and rental videos in that box and take them with you as you walk out.
  • Minimize Cell Phone Time - Somehow you survived the 80s and 90s without a cell phone. Save $75 a month or more by getting the plan that is the cheapest and hang up while driving.
  • Check Tire Pressure Weekly - Get a tire pressure gauge, or better yet, check your pressure everytime you fill up. Good tire pressure can get you another 5 MPG or more.
  • Know not just your Car's Mileage but your Dollars Per Mile - How much does it cost you to drive a mile? Is it 80 cents to the video store to rent a two dollar video? Maybe you should walk, or ride a bike.
    • Fill up, write down ODO. Drive. Fill up, write down ODO. Take Miles Driven and divide by Gallons. That is your Miles Per Gallon. Then take the price of a gallon of gas and divide by your Miles Per Gallon. That’s how much it costs to drive one mile.
  • Consolidate Trips - If you're out, get all your errands done in one trip. Avoid the "hub and spoke" model of returning home and heading out.
  • Cancel the local paper delivery - You pay for Internet, use it.
  • Cut Coupons and avoid waste - Bread lasts longest tightly bound in its bag, in the dark. Put Fruit in a paper sack in your fridge. Close lids.
  • Consolidate Insurance - Is your home and car insurance with different companies? You might get a discount if you consolidate.
  • Store Credit Cards are Satan - Cut up and close Store Cards. Call your existing Credit Cards and ask them if they can lower your rate. If they want your business, they will. Otherwise, leave them.
  • Know how your Cash Flows monthly - Monthly is usually the way to go if you're paid monthly or on the 1st and 15th. If not, find a boundary that works for you and get your life's inputs and outputs into a simple CASHIN-CASHOUT=SOMELEFTOVERCASH equation. Then, take the left over cash and save it. Take your checkbook down to some agreed upon number. We always "level off" to $300. Then next month you'll get a paycheck(s) and pay bills. Take the SOMELEFTOVERCASH-$300 and save it. Rinse, Repeat.

It was a fun evening and everyone left feeling a little more empowered and prepared to take action. If not these actions, some action.

 

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

A better PROMPT for CMD.EXE or Cool Prompt Environment Variables and a nice transparent multi-prompt

July 14, '06 Comments [10] Posted in PowerShell | ASP.NET | XML | Tools
Sponsored By

A number a people have commented on my simple, but customized command prompt. Actually I have a few, but for cmd.exe, I do this. Right-click on My Computer|Properties. Then from the Advanced Tab, click Environment Variables, then add a new User Variable called PROMPT and set it to $p$_$+$g. This came originally from Craig Andera, who got it from Shawn Van Ness. Sahil Malik also has some great command line tricks. Junfeng points out the little known ntcmds.chm.

A screenshot of the System Properties Dialogs

Here's the breakdown:

  • $P = Current Directory's Path
  • $_ = Carriage Return
  • $+ = A plus sign for each level in the PUSHD/POPD stack.

Here's some other interesting prompts. Paste these into any CMD.EXE window:

set prompt=[%computername%] $d$s$t$_$p$_$_$+$g

yields this. Note the use of an environment variables within a prompt. Here's an extensive list of environment variables.

[SCOTTPC] Thu 07/13/2006 22:19:20.40
C:\Documents and Settings\Scott

Another nice one:

set=prompt=$m$_$p$g

yields this when on a UNC Mapped Drive:

\\FREDPC\C$
Z:\Documents and Settings\Fred

There's all sorts of crap that PROMPT /? gives you:

  $A   & (Ampersand)
  $B   | (pipe)
  $C   ( (Left parenthesis)
  $D   Current date
  $E   Escape code (ASCII code 27)
  $F   ) (Right parenthesis)
  $G   > (greater-than sign)
  $H   Backspace (erases previous character)
  $L   < (less-than sign)
  $N   Current drive
  $P   Current drive and path
  $Q   = (equal sign)
  $S     (space)
  $T   Current time
  $V   Windows XP version number
  $_   Carriage return and linefeed
  $$   $ (dollar sign)
  $+   zero or more plus sign (+) characters depending upon the
       depth of the PUSHD directory stack, one character for each
       level pushed.
  $M   Displays the remote name associated with the current drive
       letter or the empty string if current drive is not a network
       drive.

Here's my command prompt. It's transparent because I use the awesome Console 2.00 Beta, build 122 hosted at SourceForge. I blogged about this last November.

Note the multiple tabs. I've got PowerShell, CMD.EXE and two VisualStudio Windows, each in their own tab. I could add a tab for Cygwin, but really, with PowerShell, who needs ls -alogF?

Scottconsole1

Console uses a XML settings file (console.xml) that takes a while to understand. Here's a snippet of my settings. I used short filenames in the VS.NET stuff for simplicity and avoidance of quoted quotes. Remember, DIR /X will give you short filenames for things like this.

...Snip...this is a PARTIAL snippet for illustrative purposes...

<tabs>
  <tab title="Console">
   <console shell="" init_dir=""/>
  </tab>
  <tab title="PowerShell">
   <console shell="c:\program files\windows powershell\v1.0\powershell.exe" init_dir=""/>
  </tab>
  <tab title="VS.NET 2003">
   <console shell="cmd /k C:\PROGRA~1\MICROS~2.NET\Common7\Tools\vsvars32.bat" init_dir=""/>
  </tab>
  <tab title="VS.NET 2005">
   <console shell="cmd /k C:\PROGRA~1\MID05A~1\VC\vcvarsall.bat" init_dir=""/>
  </tab>
 </tabs>

My font is Consolas, Size 15, Kermit Green (0x00FF00). Enjoy.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by SherWeb

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.