Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

A Better Startup Experience

flag Does Windows take too long starting up before you can get to work?  Wouldn't it better if things could startup at a lower priority?  Learn how to launch processes, use performance counters, monitor files, and run tasks in the background.
Arian's Blog

Difficulty: Intermediate
Time Required: 1-3 hours
Cost: Free
Software: Visual Basic or Visual C# Express Editions
Hardware: None
Download:

Introduction

Starting up Windows usually takes longer than I'd like it to.  It seems like the system is churning away for ages.  I've often thought that it would make more sense to wait as each item starts up before starting the next item.  The problem is Windows has no idea how long an application takes to initialize itself.  After launching it, it's just another running process.  It would be nice to be able to configure a delay after each application so it can perform its potentially expensive startup before another application starts.  I decided to put this idea into action.

This application requires Visual Studio 2005 Express Editions (C# or Visual Basic) or higher.  This will be my last Coding 4 Fun article targeting Visual Studio 2005.  If you haven't downloaded a Visual Studio 2008 Express Edition yet, definitely check them out.  The upgrade is very much worth it!

Starting Up

The basic idea of the program is very simple: launch each startup item with a configurable delay between each one.  Beyond a simple delay though, it might be good to see how active the system is.  If the CPU is working hard, maybe it would be good to wait until it settles down before launching something else.

In case you've never looked closely, there are a few ways that Windows knows what to startup with the system.  The three most common ways to start things are by using the Registry, the Startup folder, or by configuring services.  The Registry and Startup folder methods are pretty similar.  Of course Services are background processes that run even if a user isn't logged in.

Once a user logs in, the Registry and Startup folders take effect.  The system Registry is a collection of keys, values, and data that applications (and Windows itself) use for configuration.  One such use is to contain a list of programs to launch with Windows.  There is a system-level and user-level list.  System-level programs are started up for every user that logs in, while user-level programs are started only for the specific user.

The Startup folder works the same way.  As part of each user's profile folder there is a Startup folder.  There is also one at the Default User level.  These folders contain shortcuts to applications.  Whenever a user logs in, all of these shortcuts are launched.  We just need to take over this process.  To do this though, we'll need to take the shortcuts out of the system Startup folders.

The Basics

The Startup Optimizer works by scanning the user's Startup folder looking for shortcuts.  As a new shortcut is found, a StartupEntry object is created.  This object holds the filename, a display name and a few flags.  The key flag is Managed.  An object with Managed set to True is launched by the application and moved to a new ManagedStartup folder in the user's profile.

When a file or shortcut is detected in the Startup folder, a popup will occur to let a user decide if the program should manage it or not.  If so, it moves the shortcut to its ManagedStartup folder.  If not, it leaves it alone but sets the Managed flag to False.  Next time it notices the same shortcut, it will just now to ignore it and let Windows handle it.

Startup Item Encountered

Figure 1: The dialog shown when a startup item is found

If you click the button to show the advanced options, you can specify a display name, the low CPU flag, and how to long to wait before launching the next application.

Startup Item Encountered - Advanced Settings

Figure 2: The dialog with advanced settings shown 

Notice the icon.  There's no included support for extracting the icon from a file, but you can use the Win32 SHGetFileInfo function in the shell32.dll library like so:

Visual Basic

<DllImport("shell32.dll")> _
Public Shared Function SHGetFileInfo(ByVal pszPath As String, ByVal dwFileAttributes As UInteger, ByRef psfi As SHFILEINFO, ByVal cbSizeFileInfo As UInteger, ByVal uFlags As UInteger) As IntPtr
End Function

Visual C#

[DllImport("shell32.dll")] 
public static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes,
       ref SHFILEINFO psfi, uint cbSizeFileInfo, uint uFlags);

All StartupEntry objects are added to the StartupEntryList collection which is a subclass of List<StartupEntry>.  Creating a subclass like this allows you to add helper methods for finding or removing objects or other useful functions.  This collection is what is saved to disk to retain the properties of each item.  The basic .NET binary serialization method is used due to its simplicity in code.  Note that any class to be serialized must be decorated with the Serializable attribute.

Visual Basic

Public Shared Function FromDisk() As StartupEntryList
    If Not File.Exists("StartupEntryList.ser") Then
        Return New StartupEntryList()
    End If

    Dim c As StartupEntryList
    Dim s As Stream = File.Open("StartupEntryList.ser", FileMode.Open)
    Dim b As New BinaryFormatter()
    c = DirectCast(b.Deserialize(s), StartupEntryList)

    s.Close()

    Return c
End Function

Public Sub SaveToDisk()
    Dim s As Stream = File.Open("StartupEntryList.ser", FileMode.Create)
    Dim b As New BinaryFormatter()
    b.Serialize(s, Me)
    s.Close()
End Sub

Visual C#

public static StartupEntryList FromDisk() 
{ 
    if (!File.Exists("StartupEntryList.ser")) return new StartupEntryList(); 

    StartupEntryList c; 
    Stream s = File.Open("StartupEntryList.ser", FileMode.Open); 
    BinaryFormatter b = new BinaryFormatter(); 
    c = (StartupEntryList)b.Deserialize(s); 

    s.Close(); 

    return c; 
} 

public void SaveToDisk() 
{ 
    Stream s = File.Open("StartupEntryList.ser", FileMode.Create); 
    BinaryFormatter b = new BinaryFormatter(); 
    b.Serialize(s, this); 
    s.Close(); 
} 

Notice how the FromDisk() method is static.  This works as a factory method on the StartupEntryList class itself.  Subclassing that simple generic list has made it easy to encapsulate all related functionality into the same place.

Working in the Background

Of course at some point the application needs to perform its startup magic.  Obviously the main work is just launching each shortcut.  This is done using the Process.Start() method in the System.Diagnostics namespace.  If we were just launching the shortcuts one after another, it would be over quickly, but it would defeat the purpose of the program!  With delays and waiting for the CPU to go idle, this startup process could potentially take a few minutes.  This is important because it means that we can't just start the sequence in the Form_Load event handler on startup.  The application would be unresponsive and there would be no way to abort.

Enter the BackgroundWorker class.  I've used this in articles before and they make it really easy to run things in the background.  It's much easier than creating threads -- especially if you need to update the user interface.  You get to avoid the Invoke calls that are otherwise required and you even get Form Designer support.

Similar to the StartupEntryList, subclassing is a good idea in order to keep things nicely encapsulated.  There's actually only one method that matters in this case: the event handler for the DoWork event.  This is where work is done.  You never call it directly (it's marked private) -- it's invoked when the RunWorkerAsync method is called.  Anything that happens in the event handler is automatically on a thread other than the UI thread.  It couldn't be simpler!  In order to cause the UI to be updated (you can't ever directly reference it due to cross-threading concerns) call the ReportProgress method.  Periodically check the CancellationPending flag to see if you need to exit.

The StartupBackgroundWorker_DoWork event handler method basically just iterates over the entries that have Managed set to true, waits for an idle CPU if necessary, and waits the specified delay time if set before moving to the next one.  At several points it checks to see if a cancellation has been requested.  The best thing though, is regardless of how long the sequence takes the UI will run smoothly.

Visual Basic

    Private Sub StartupBackgroundWorker_DoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs)
        Dim entries As List(Of StartupEntry) = TryCast(e.Argument, List(Of StartupEntry))

        If entries Is Nothing OrElse entries.Count = 0 Then
            e.Cancel = True
            Throw New Exception("No entries passed in")
        End If

        Try
            ' Don't bother if there aren't any entries!
            If entries.Count > 0 Then
                Me.ReportProgress(0, "Startup Optimizer will begin in five seconds.  Click to cancel.")
                Dim i As Integer = 0
                While i < 5 AndAlso Not Me.CancellationPending
                    Thread.Sleep(1000)
                    i += 1
                End While

                If Me.CancellationPending Then
                    e.Cancel = True
                    Return
                End If
            End If

            systemIdleComponent1.Enabled = True

            ' Process entries to perform startup
            For Each entry As StartupEntry In entries
                If entry.Managed Then
                    If entry.WaitForLowCPU Then
                        Me.ReportProgress(0, "Waiting for idle CPU to launch: " + entry.DisplayName)

                        ' CPU wait routine...
                        ' TODO: Give up if we never go idle?  This would be the place..!
                        While Not systemIdleComponent1.WaitForIdle(1000)
                            If Me.CancellationPending Then
                                e.Cancel = True
                                Return
                            End If
                        End While
                    End If

                    Me.ReportProgress(0, "Starting: " + entry.DisplayName)

                    ' Don't actually start if we are debugging
                    If Not Debugger.IsAttached Then
                        Process.Start(Path.Combine(My.Settings.ManagedStartupPath, entry.FileName))
                    Else
                        'SIMULATE STARTUP DELAY
                        Thread.Sleep(2000)
                    End If

                    Me.ReportProgress(0, "Started: " + entry.DisplayName)

                    If entry.DelayTime > 0 Then
                        Me.ReportProgress(0, "Waiting: " + entry.DelayTime + " seconds until next item")
                        For x As Integer = 0 To entry.DelayTime - 1

                            ' Wait the number of seconds but check for cancel flag each second
                            If Me.CancellationPending Then
                                e.Cancel = True
                                Return
                            End If

                            Thread.Sleep(1000)
                        Next
                    End If
                End If
            Next
        Catch
            e.Cancel = True
        Finally
            systemIdleComponent1.Enabled = False
        End Try

    End Sub

Visual C#

private void StartupBackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
    List<StartupEntry> entries = e.Argument as List<StartupEntry>;

    if (entries == null || entries.Count == 0 )
    {
        e.Cancel = true;
        throw new Exception("No entries passed in");
    }

    try
    {
        // Don't bother if there aren't any entries!
        if (entries.Count > 0)
        {
            this.ReportProgress(0, "Startup Optimizer will begin in five seconds.  Click to cancel.");
            for (int i = 0; i < 5 && !this.CancellationPending; i++)
                Thread.Sleep(1000);

            if (this.CancellationPending)
            {
                e.Cancel = true;
                return;
            }
        }

        systemIdleComponent1.Enabled = true;

        // Process entries to perform startup
        foreach (StartupEntry entry in entries)
        {
            if (entry.Managed)
            {
                if (entry.WaitForLowCPU)
                {
                    this.ReportProgress(0, "Waiting for idle CPU to launch: " + entry.DisplayName);

                    // CPU wait routine...
                    // TODO: Give up if we never go idle?  This would be the place..!
                    while( !systemIdleComponent1.WaitForIdle(1000) )
                    {
                        if (this.CancellationPending)
                        {
                            e.Cancel = true;
                            return;
                        }
                    }
                }

                this.ReportProgress(0, "Starting: " + entry.DisplayName);

                // Don't actually start if we are debugging
                if( !Debugger.IsAttached)
                    Process.Start(Path.Combine(Properties.Settings.Default.ManagedStartupPath, entry.FileName));
                else
                    Thread.Sleep(2000); //SIMULATE STARTUP DELAY

                this.ReportProgress(0, "Started: " + entry.DisplayName);

                if (entry.DelayTime > 0)
                {
                    this.ReportProgress(0, "Waiting: " + entry.DelayTime + " seconds until next item");

                    // Wait the number of seconds but check for cancel flag each second
                    for (int x = 0; x < entry.DelayTime; x++)
                    {
                        if (this.CancellationPending)
                        {
                            e.Cancel = true;
                            return;
                        }

                        Thread.Sleep(1000);
                    }
                }
            }
        }
    }
    catch
    {
        e.Cancel = true;
    }
    finally
    {
        systemIdleComponent1.Enabled = false;
    }

}

There are a few things to callout in this block.  I have a few sanity checks in there to be sure that the list isn't NULL or empty.  Defensive programming is always a good idea!  Notice also that I have pauses built in with the Thread.Sleep() command.  Where it would be convenient to pause for several seconds, it would render the thread unresponsive if a cancellation was requested.  Note that cancellation is completely passive.  The framework doesn't ever abort your BackgroundWorker.  It's up to your code to catch it if cancellation is requested.  So for pauses, I wait a second, check the flag, and repeat for the number of seconds.  The code is slightly bulky but it works well.

Notice also how I don't launch the startup applications if System.Diagnostics.Debugger.IsAttached is true.  When I'm troubleshooting/testing, I don't really want the applications launching every time!  It's a simple check to see if the application is currently being debugged (as in Visual Studio).

Detecting Idleness

In addition to the simple time-based delays, I also wanted the option to wait for the system to be idle before launching applications.  There's no direct way to see this though, so I created the SystemIdleComponent.  This is completely modular and could be plugged into other applications easily.  It all comes down to system Performance Counters.  If you've never worked with these before, don't let them intimidate you.  They're actually very easy to work with.  From the Visual Studio Toolbox you can easily drag a counter and set its properties (category, counter,and instance), but unfortunately there's no support for events when the value changes.  A pretty big deal I think!

My component builds on the PerformanceCounter object and creates a System.Threading.Timer object behind the scenes to monitor the state of the counter periodically.  If a change is measured and it falls outside of the tolerance limits, it raises an event.  It also raises an event when it returns to the tolerance.  This allows you to receive an event both when the system becomes idle and when it returns to non-idle.

Visual Basic

    Private Sub timerCpu_Tick(ByVal state As Object)
        Dim currentValue As Integer = 100 - CInt(performanceCounterProcessorIdle.NextValue())

        ' If we are below the threshold, start counting...
        If currentValue <= _thresholdIdlePercent Then
            idleTimer.Start()
        Else
            idleTimer.[Stop]()
            idleTimer.Reset()
        End If


        ' See if we've been idle long enough
        If idleTimer.ElapsedMilliseconds / 1000 > _thresholdIdleTime Then
            ' We've just gone idle
            RaiseIdleEvent()
        Else
            ' We are not idle
            LowerIdleEvent()
        End If

        _lastCpuValue = CInt(currentValue)
    End Sub

Visual C#

private void timerCpu_Tick(object state)
{
    int currentValue = 100 - (int)performanceCounterProcessorIdle.NextValue();

    // If we are below the threshold, start counting...
    if (currentValue <= _thresholdIdlePercent)
        idleTimer.Start();
    else
    {
        idleTimer.Stop();
        idleTimer.Reset();
    };

    // See if we've been idle long enough
    if (idleTimer.ElapsedMilliseconds / 1000 > _thresholdIdleTime)
    {
        // We've just gone idle
        RaiseIdleEvent();
    }
    else
    {
        // We are not idle
        LowerIdleEvent();
    }

    _lastCpuValue = (int)currentValue;
}

What is idle?  This is defined by properties ThresholdIdlePercent and ThresholdIdleTime.  The statement is "the system is idle when it has been at {ThresholdIdlePercent}% for {ThresholdIdleTime} seconds."  A consuming application just sets the properties and subscribes to events.  Alternately, you can check the SystemIdle property when you need to.  A final way is to call WaitForIdle with our without a timeout value.  The call will not return until the system is idle.

This works by using an System.Threading.EventWaitHandle object.  This very cool construct (system-based, not just .NET) lets two threads coordinate via a shared semaphore.  The BackgroundWorker thread goes to sleep until the SystemIdleComponent's timer fires and detects an idle state.  It can be dangerous to wait too long locked like this, so the worker will only call it for a second at a time in-between checking for a cancellation request.  I've added additional attributes to this object to make it easier to reuse and for a good design-time experience.  Drag it to your form and work with it like any other object.

Usage

It currently reads the user's Startup folder upon startup and shows the dialog to take action.  There's no Cancel button since it wouldn't make sense.  If you want to cancel, chances are you really just don't want the application to manage that shortcut.  It won't know that next time unless it creates an entry for it though.

Something to think about is that some applications are going to check the Startup folder every time they launch and add their shortcut if it isn't there.  Once that StartupEntry is in the StartupEntryList collection, the application will know what to do with it and you won't be prompted again.

A FileSystemWatcher object enables the application to see new shortcuts added.  It then takes action only on shortcuts for which no StartupEntry is found.  You can modify the display name, delay, wait-for-idle flag, or the managed property at any time.  Unless you're debugging, you won't have any way to execute the launch sequence except when it's initially started with the "/startup" command line flag.  If you launch it without the flag it won't launch anything so it's safer that way.

There's not much else to worry about.  Tell it which startup entries to manage, then let it do it's thing upon restarts.  It could really exit after the list is complete, except that it wouldn't see new entries that way.

Conclusion

The application ended up taking considerably longer than I ever imagined it would!  I tried to create a good experience while demonstrating some good concepts.  I was surprised at some of the subtleties I ran into to make this work as well as it does.  It's not perfect by any means, but it's a good start and it should be useable as-is.

I tried to keep things out of MainForm where it made sense, though there is still room for more encapsulation.  Individual classes should always be fairly light-weight.  It's the way that they interact with each other that provides their power.

Next Steps

For this article, you can use Visual Studio 2005 Express, or make the upgrade to Visual Studio 2008 Express.  As I mentioned at the beginning, I'll be upgrading to VS2008 for future articles, and with the Express products being free, there's no reason not to upgrade today at http://www.microsoft.com/express/.

I left a number of TODO comments in the code.  These were cool things that I just didn't have time to implement.  For one thing, it would be good to be able to just drag an application to the main window to add it to the Managed Startup list.  Even more useful would be scanning the Registry Current User (HKCU) Run list.  Probably more entries end up there than the Startup folder.  I'd also like to extend my SystemIdleComponent to be a generalized PerformanceCounter event broker.  That would make it easy to add code to wait for low disk I/O in addition to CPU load.  Finally, I really wanted to add entry reordering to specify which applications should have the higher priority of starting first.  Not difficult, so give it a try!  If you end up adding any interesting features, let me know.  I'd love to see where it goes!


Arian Kulp
Arian Kulp is an independent software developer and writer working in the Midwest.  He has been coding since the fifth grade on various platforms, and also enjoys photography, nature, and spending time with his family.  Arian can be reached through his web site at http://www.ariankulp.com.

Tags:

Follow the Discussion

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.