Saturday, March 30, 2013

Is this volume offline?

Apologies

First of all, let me apologize for this silence since June 2012. In August 2012, my wife and I decided to go back in France with our daughter. Moving from Africa to Europe was not as seamless as we thought it would be. But everything is now back in place and my french company is now running since March, 1st.

First project!

For my first project as a French company, one of the constraints is: cross-platform. Mac OS x and Windows for sure, and at some point it will have to run on Linux. Given that, the IDE choice was simple: Real Studio. This application will have to watch for remote volumes and if it finds a specific folder and file hierarchy, then it will have to copy some of the files to another specific file structure that resides on local drives. As the remote volumes are not always on line, the application needs to know when a drive is mounted or unmounted.

I searched for a ready-to-use cross-platform solution to accomplish that. I also ask the NUG maillist and I found the Windows Functionality Suite or MacOsLib that are avalaible for free but that come with poor documentation, if any. There is the MBS plugins, documented, but still not really cross-platform. And I'm always lost in its documentation. Then I thought about something Charles Yeomans once wrote on the NUG. I don't remember the exact words but he said that there is nothing a plugin can do that can't be done in Real Studio with or without Declare. So I decided to write my own solution in pure REALbasic.

In pure REALbasic?

As a prototype, I created a Thread subclass that I named zdVolumeWatcher and add two event definitions.

Event VolumeMounted(inNativePath As String, inFolderItem As FolderItem)
Event VolumeRemoved(inNativePath As String)

Then to remember what was the configuration at the last check I decided to use a dictionary and to make it private as it's to be used by the class only.

Private pVolumeList As Dictionary

The key will be the shell path of the volume, as on MacOS X we can be sure that it's unique even if there are volumes with the same name. The value will be the FolderItem returned by the Volume() function. The dictionary will be initialized in the class constructor.

Sub Constructor()
  // Create the volume list dictionary
  Me.pVolumeList = New Dictionary
End Sub

Let's go for the Run() event code. After declaring the variables the code will use, it enter an endless loop. Not quite endless indeed but we'll discuss that later. The first thing the code is doing is to collect all the volumes returned by the Volume() function, store them in an array of FolderItem and also store the volume's shell path in an array of String.

Then the code searches for new volumes. It checks that the shell path of each collected element already exist in the pVolumesList() dictionary. If not, the volume's FolderItem is added to the dictionary using its shell path as key and the VolumeMounted event is raised.

Last step, the code searches for removed volumes. This time it uses the pVolumeList dictionary and try to find their shell path in the thePaths() array. If it failed to find it, this path is removed from the dictionary and the VolumeRemoved event is raised

Sub Run()
  //-- Watch the attached volumes

  #pragma DisableBackgroundTasks

  // Setup the config
  Dim theVolumes(), theItem As FolderItem
  Dim thePaths(), thePath, theKey As String
  Dim theLastIndex, theIndex As Integer

  // Let's go for an endless loop
  Do
 // --- Collect the volumes ---

 // Redim the arrays
 theLastIndex = VolumeCount - 1
 Redim theVolumes( theLastIndex )
 redim thePaths( theLastIndex )

 // Fill the volume arrays from the
 For theIndex = 0 to theLastIndex

   theItem = Volume( theIndex )
   theVolumes( theIndex ) = theItem
   thePaths( theIndex ) = theItem.ShellPath

 Next

 // --- Check for New Volumes ---

 theLastIndex = thePaths.Ubound
 For theIndex = 0 to theLastIndex

   // Retrieve the volume path
   thePath = thePaths( theIndex )

   // Was it there last time we checked?
   If Not Me.pVolumeList.HasKey( thePath ) Then

  // No, this is a new volume, add it to our volume list...
  Me.pVolumeList.Value( thePath ) = theVolumes( theIndex )

  // ... And tell the world "Habemus volumus"
  RaiseEvent VolumeMounted( thePath, theVolumes( theIndex ) )

   End If
 Next

 // --- Check for removed volumes ---

 For theIndex = 0 to Me.pVolumeList.Count - 1

   // Retrieve this known volume Key ( Shell Path )
   theKey = Me.pVolumeList.Key( theIndex )

   // Is it still there?
   If thePaths.IndexOf( theKey ) < 0 Then

  // No, this volume has been removed
  // Tell the world...
  RaiseEvent VolumeRemoved( theKey )
  // ... and renove it from our list
  Me.pVolumeList.Remove( theKey )

   End If

 Next

 // Let's get some sleep
 Me.Sleep( 1000 ) // 1000 msecs = 1 sec.

 // Loop until being told to stop
  Loop Until Me.pStopFlag

  // Some housekeeping never hurt
  Me.pVolumeList = Nil
  theItem = Nil
  Redim theVolumes( -1 )
  Redim thePaths( -1 )
  // We're done here.
End Sub

You may have noticed the Loop Until Me.pStopFlag line. pStopFlag is a private property that is set in a method called StopNicely. If you want to stop the thread nicely, just call this method. It will stop it at the end of a checking loop, not in the middle of it.

Sub StopNicely()
  //-- Sets the stop flag
  Me.pStopFlag = True
End Sub

So, the thread checks the volumes configuration each second or so. Be aware that if a volume is connected and disconnected while the thread is sleeping, nothing will happen. You still can set the thread not to sleep, but it will just consume CPU power.

Using it for real

To use zdVolumeWatcher, you can subclass it to directly access the Event or use the AddHandler keyword to delegate the Event handling to one of your own methods.

Thread vs. Timer to update the GUI

Well, in the project I will use this class to send notifications to a 'NotificationCenter' class of my own, also written in pure REALbasic. This class will spread the notification in the others elements of the application. What happens if you just want to use zdVolumeWatcher to directly update the user interface and don't want to deal by the Threads can't interact directly with GUI thing?

Very simple to do... Duplicate the zdVolumeWatcher class and rename the copy to zdVolumeWatcherTimer. Now, cut & copy the code from the Run() event to the action event. A this point, the Run() event should be gone. In the Action() event, delete the line with Do at the beginning of the loop and the all the lines at the end of the loop and listed below.

 // Let's get some sleep
 Me.Sleep( 1000 ) // 1000 msecs = 1 sec.

 // Loop until being told to stop
  Loop Until Me.pStopFlag

  // Some housekeeping never hurt
  Me.pVolumeList = Nil
  theItem = Nil
  Redim theVolumes( -1 )
  Redim thePaths( -1 )

You can also remove the pStopFlag property and the StopNicely method. Now you can drop the zdVolumeWatcherTimer on a window layout and fill the VolumeMounted and VolumeRemoved events to intract with the GUI without any problem.

These classes are not perfect, and their code could be improved for sure. But the project is a young. If you see some improvements, feel free to post a comment. I'd be glad to update this post with your suggestions.

No comments:

Post a Comment