Click here to Skip to main content
Click here to Skip to main content

Oracle Service Manager

, 13 May 2004 CPOL
Rate this:
Please Sign up or sign in to vote.
Manage the state of Oracle services to conserve resources -- just like Microsoft SQL Server Service Manager.

Sample Image - OracleServiceManager01.jpg

Introduction

My goal is to create a Microsoft Windows utility to watch and administer, in a simple manner, Oracle Windows services on local and remote machines. The baseline operation of the utility mirrors the Service Manager that ships with Microsoft SQL Server 2000. I used Visual Studio .NET 2003 (.NET Framework 1.1.4322).

There are two personal reasons for building this tool: the first is to gain experience with design patterns; and the second is to help manage system resources. Oracle consumes significant machine resources. So, I shut down the Oracle services to help free up system resources for other development or machine tasks.

This tool will provide a quick and easy way to see the state of any Oracle Windows service and to start, pause, and stop that service.

Application Analysis

Oracle Service Manager (OSM) offers the user three mechanisms to control the services; two are user interfaces while the third is a “passive” background device:

  1. A traditional dialog interface;
  2. A system tray menu for quick access; and
  3. A timer to poll the service for state changes introduced elsewhere (e.g., using the Services Control Panel applet, or using one of the Oracle tools).

Analyzing the needs of each device above, reveals unique behavioral attributes. None operate in a vacuum as changes in one affect or update the other. For example, changing the service in the dialog changes the service represented in the tray menu, as well as the service polled by the timer. Similarly, starting the service, from the tray menu, results in interface updates in the dialog.

At this point, it is worth noting the default way Visual Studio wants you to program this tool which is to create a Windows Application (the dialog form), and place NotifyIcon and Timer components on the form. I consider this a trap of “visual programming”. In other words, this approach tightly couples the dialog, system tray and timer code. This “visual” approach usually results in code that is hard to read, hard to maintain, and difficult to enhance or debug. By keeping these components separate and distinct using well known design patterns, these problems can be addressed resulting in a very extensible solution.

Points of Interest

OSM presents us with two primary issues to solve. The first is about watching or observing, the state of a Windows service. The second is about the actions that can be taken on the Windows service.

These patterns are encapsulated in the accompanying ServiceStatePublisher library, described in detail elsewhere on this site. OSM will use this library extensively.

This was the first Windows GUI application I programmed using C# and .NET. I was dumbfounded by the size of the InitializeComponent function for such a small dialog. I decided to try my luck at crafting this function by hand in the hopes of eventually applying a design pattern to reduce its size. The cost of hand-building the dialog would be debilitating the Windows Forms Designer.

Being curious as to how much hand construction costs, I decided to build the DialogObserver and SystemTrayObserver UI components by hand. These components are recognizable by the region tags which indicate “Manually generated Form code”. As a result, these forms cannot be read by the Windows Forms Designer. In contrast, I let Windows Forms Designer have its way when creating the About and Options dialogs as these are trivial forms.

I am once again excited about the possibilities of the Visual Studio .NET 2005 release, code named “Whidbey”. This problem associated with Designer generated code may be partially, no pun intended, solved with partial classes. The other mechanism that should help significantly is “Longhorn” and the use of XAML.

Below, I explain the main application parts, design decisions, and implementation.

The Main Object -- OracleServiceManager

I have chosen not to use the dialog as the startup object. Instead, I have used my own startup object OracleServiceManager, derived from ApplicationContext.

  1. After looking at the inheritance chain for System.Windows.Forms.Form, it seemed natural to use ApplicationContext as the base class because it provides the ExitThread function needed to properly exit the application.
    public class OracleServiceManager : ApplicationContext
    {
        private static SingleInstance _singleInstance;
        private static OracleServiceManager _instance;
        private ServiceSubject _subject;
        private SystemTrayObserver _trayObserver;
        private DialogObserver _dialogObserver;
        private TimerObserver _timerObserver;
        private OsmResources _resources;
  2. A single instance of the application is enforced through the Singleton pattern. I have created my own class, but there are other great examples of the Singleton pattern on this site. The Singleton pattern is supported using the SingleInstance class. This class uses a mutex and class GUID to help enforce a single application instance across all OS processes.
        static OracleServiceManager()
        {
            Type type = typeof(OracleServiceManager);
            _singleInstance = new SingleInstance( type.GUID);
            _instance = new OracleServiceManager();
        }
    
        internal static OracleServiceManager Instance
        {
            get { return _instance; }
        }
  3. The application controller, OracleServiceManager, manages the lifetimes of several objects including:
    • OsmResources for localization – described further below in more detail;
    • ServiceSubject for the Observer Pattern Subject – described in the ServiceStatePublisher article; and
    • DialogObserver, SystemTrayObserver, and TimerObserver for, you guessed it, the concrete observers.
        private void InitializeApplication()
        {
            LoadConfig();
            _resources = new OsmResources();
    
            _title = GetAssemblyTitle();
            _subject = new ServiceSubject();
    
            _dialogObserver = new DialogObserver( _subject);
            _trayObserver = new SystemTrayObserver( _subject);
            _timerObserver = new TimerObserver( _subject, _pollingInterval, 
                                      _dialogObserver.SynchronizingObject);
    
            Subject.ServerName = Environment.MachineName;
        }
    
        internal OsmResources Resources
        {
           get { return (_resources != null ? _resources : new OsmResources()); }
        }
    
        internal ServiceSubject Subject
        {
            get { return _subject; }
        }
  4. A static Main method provides an entry point. But remember, the static constructor detailed above, runs before Main is called – which can make things a bit tricky.
        [STAThread]
        static void Main( string[] args)
        {
            if (!_singleInstance.IsRunning)
            {
                if ( args.Length > 0)
                    Instance.SetThreadUICulture( args[0]);
    
                Instance.InitializeApplication();
                Application.Run( OracleServiceManager.Instance);
    
                _singleInstance.Dispose();
            }
            else
              MessageBox.Show( Instance.Resources.GetAppString( "AlreadyRunning"),
                                Instance.Resources.ApplicationName,
                                MessageBoxButtons.OK, MessageBoxIcon.Information);
        }
    }

The Observers / Subscribers

There are three (3) observers: DialogObserver, SystemTrayObserver, and TimerObserver The DialogObserver and SystemTrayObserver are so similar, I will only discuss one below. I will also discuss the TimerObserver. While it too is similar, TimerObserver is interesting because it has no interface and has some interesting challenges and decisions.

DialogObserver

  1. The System.Windows.Forms.Form class provides the base functionality for the dialog.
    using System.Windows.Forms;
    
    public class DialogObserver : Form
    {
  2. The observer works by subscribing to the subject; asking the subject to notify it of any changes in state. The observer only responds to changes once notified by the subject – even if the observer was the cause of the change. To support the complex interactions in the dialog, two delegates are declared – UpdateFunction and ServiceAction. UpdateFunction is used to prevent the cascading events generated by message-driven programming. ServiceAction, on the other hand, is used to broadly control the dialog when performing an action on a service, such as starting, pausing, or stopping.
        protected delegate void UpdateFunction( ServiceSubject subject);
        protected delegate void ServiceAction();
    
        private bool _updating;
        private UpdateFunction serverChanged;
        private UpdateFunction serviceChanged;
        private UpdateFunction serviceStateChanged;
        private ServiceAction startService;
        private ServiceAction pauseService;
        private ServiceAction stopService;
    
        public DialogObserver( ServiceSubject subject)
        {
            if (subject != null)
            {
                subject.ServerChanged += new EventHandler( this.OnServerChanged);
                subject.ServiceChanged += new EventHandler( this.OnServiceChanged);
                subject.StateChanged += new EventHandler( this.OnStateChanged);
            }
    
            serverChanged = new UpdateFunction( this.ServerChanged);
            serviceChanged = new UpdateFunction( this.ServiceChanged);
            serviceStateChanged = new UpdateFunction( this.ServiceStateChanged);
    
            startService = new ServiceAction( App.Subject.Start);
            pauseService = new ServiceAction( App.Subject.Pause);
            stopService = new ServiceAction( App.Subject.Stop);
    
            InitializeComponent();
        }
  3. In preparation for the timer observer, there will be a need to marshal timer events on a worker thread back to the GUI thread. So, I have provided a property to use from the dialog observer. This property is used by the controller, OracleServiceManager. The property is passed to the TimerObserver constructor as one of the optional parameters.

    (See OracleServerManager::InitializeApplication() above.)

        internal ISynchronizeInvoke SynchronizingObject
        {
            get { return serviceCombo; }
        }
  4. The implementation of the subscription events has one twist. All the events change the dialog controls which produces the cascading events problem found in message-driven GUI programming. I used the delegate mechanism to supply one function that stops the cascading effect. This results in almost identical subscription events.

    I suppose I could have created my own EventArgs class and added a Cause attribute. This would have brought the subscription events from three functions down to one. The consequence of this approach is a conditional statement – an if or switch statement – to discern the cause and act accordingly. Personally, I try to limit my use of conditionals – they tend to be sources of errors for me. Of course, I don’t expect to eliminate their use – that’s not possible – but I try to keep any conditions as simple as possible. [Off soapbox].

        protected void OnServerChanged( object sender, System.EventArgs e)
        {
            ServiceSubject subject = (ServiceSubject) sender;
            Update( serverChanged, subject);
        }
    
        protected void OnServiceChanged( object sender, System.EventArgs e)
        {
            ServiceSubject subject = (ServiceSubject) sender;
            Update( serviceChanged, subject);
        }
    
        protected void OnStateChanged( object sender, System.EventArgs e)
        {
            ServiceSubject subject = (ServiceSubject) sender;
            Update( serviceStateChanged, subject);
        }
    
        private void Update( UpdateFunction function, ServiceSubject subject)
        {
            if (!Updating)
            {
                Updating = true;
    
                function( subject);
    
                Updating = false;
            }
        }
    
        private void ServerChanged( ServiceSubject subject)
        {
            ClearServiceItems();
    
            if (subject.ServerName != null)
            {
                AddServer( subject.ServerName);
                SetServerItem( subject.ServerName);
                SetStatusBarText( GetStringFromResources( "Connected"), 
                                                     subject.ServerName);
                AddOracleServicesToList( subject.ServerName);
            }
            else
            {
                if (serverCombo.Text.Length > 0)
                    SetStatusBarText( GetStringFromResources( "NotConnected"),
                                      serverCombo.Text);
                else
                    SetStatusBarText( GetStringFromResources( "Ready"));
            }
        }
    
        private void ServiceChanged( ServiceSubject subject)
        {
            if (subject.Controller != null)
            {
                SetServiceItem( subject.Controller.DisplayName);
                ServiceStateChanged( subject);
            }
            else
            {
                DisableActionButtons();
                SetDefaultServiceStateImage();
            }
        }
    
        private void ServiceStateChanged( ServiceSubject subject)
        {
            if (subject.State != ServiceStateUnknown.Instance)
            {
                SetActionButtonState( subject);
                SetServiceStateImage( subject.State);
                SetStatusBarText( subject.State.Status, subject.ServerName,
                                  subject.Controller.ServiceName);
            }
            else
            {
                DisableActionButtons();
                SetDefaultServiceStateImage();
            }
        }
  5. The following code contains the dialog events to which we respond. There are a couple of points here. First, I intercept the Closing event and do not allow the dialog to close, but rather hide it. Most of the work in the dialog is in ServerComboValidating and ActionClick. The former checks if the chosen server name is valid AND there is a machine on the network with that name – this is done in a supporting function IsServerValid. ActionClick, on the other hand, is another use of a delegate to streamline programming. That is, ActionClick encapsulates the cursor changes, and dialog enabling/disabling when performing an action like start, stop or pause.
        private void ClosingDialog( object sender, CancelEventArgs e)
        {
            if (sender == this)
            {
                e.Cancel = true;
                this.Hide();
            }
        }
    
        private void ServerComboValidating( object sender, CancelEventArgs e)
        {
            string serverName = serverCombo.Text.ToUpper();
            ServiceSubject subject = App.Subject;
    
            if (subject.ServerName != serverName)
            {
                SetStatusBarText( GetStringFromResources( "Connecting"), 
                                                             serverName);
                if (IsServerValid( serverName))
                {
                    subject.ServerName = serverName;
                }
                else
                {
                    string msg = GetStringFromResources( "NoNetwork");
                    MessageBox.Show( this, msg, App.Title, MessageBoxButtons.OK, 
                                     MessageBoxIcon.Exclamation);
                    subject.ServerName = null;
                    e.Cancel = true;
                }
            }
        }
    
        private void ServiceComboChangedIndex( object sender, EventArgs e)
        {
            ServiceSubject subject = App.Subject;
    
            subject.Controller = new ServiceController( serviceCombo.Text,
                                                        subject.ServerName);
        }
    
        private void RefreshClick( object sender, EventArgs e)
        {
            App.Subject.RefreshServices();
        }
    
        private void StartClick( object sender, EventArgs e)
        {
            ActionClick( startService, START);
        }
    
        private void PauseClick( object sender, EventArgs e)
        {
            ActionClick( pauseService, PAUSE);
        }
    
        private void StopClick( object sender, EventArgs e)
        {
            ActionClick( stopService, STOP);
        }
    
        private void ActionClick( ServiceAction action, string actionName)
        {
            Cursor.Current = Cursors.WaitCursor;
            if (VerifyUserAction( actionName))
            {
                this.Enabled = false;
                action();
                this.Enabled = true;
            }
            Cursor.Current = Cursors.Default;
        }
  6. There are a number of supporting functions which are in the region labeled Dialog Support Functions. If you’re interested, take a look. Most of the support functions do small, but important, tasks – like controlling the statusbar caption, and the enabled status of controls, etc. I suspect most people reading this article will have a strong grasp on these concepts, so I have not included them. If the feedback indicates I should discuss them, I’ll update the article.

TimerObserver

  1. This observer does not derive from any base class, instead it uses one of the three timers provided by the .NET Framework – specifically, the System.Timers.Timer class. This Timer class uses .NET worker threads. In addition, the resolution offered by this class is more than sufficient for my needs. However, the class is built in such a way that the other two timers could be used instead with little rework.

    This observer has some other interesting characteristics. The one I’d like to call attention to is the delegate ChangeStateCallback that assists in marshalling the event back to the GUI thread. Also, note the Dispose method which cleans up and discards the timer object.

    Please note: I have removed any references to the System.Diagnostics.Debug object for clarity. The code in the project still has these references – and you may find it useful if you wish to see how the messages are processed.

    public class TimerObserver
    {
        private delegate void ChangeStateCallback( ServiceState state);
    
        private System.Timers.Timer _timer;
        private DateTime _stopTime;
        private ServiceState _state;
        private ISynchronizeInvoke _syncObject;
        private ChangeStateCallback changeStateCallback;
    
        public TimerObserver( ServiceSubject subject, double interval) :
             this( subject, interval, null)
        {
        }
    
        public TimerObserver( ServiceSubject subject, double interval,
                              ISynchronizeInvoke synchronizingObject)
        {
            if (subject != null)
            {
                subject.ServerChanged += new EventHandler( this.OnServerChanged);
                subject.ServiceChanged += new EventHandler( this.OnServiceChanged);
                subject.StateChanged += new EventHandler( this.OnStateChanged);
            }
    
            _timer = new System.Timers.Timer( interval);
            _timer.Elapsed += new ElapsedEventHandler( this.OnTimerElapsed);
                
            SynchronizingObject = synchronizingObject;
    
            changeStateCallback = new ChangeStateCallback( this.ChangeState);
        }
    
        public void Dispose()
        {
            StopTimer();
            _timer.Dispose();
            _timer = null;
        }
  2. The Elapsed event has three interesting points. First, it is called on the worker thread. Second, as a result, it needs to decide – based on the existence of a synchronizing object – if it must marshal back to the GUI thread. And third, as noted in the documentation and elsewhere on this site, this timer has a side-effect of possibly being called after it was told to stop. This could raise an event that we do not want. Solving this issue is pretty straightforward – track the stop time and compare it to the event signal time.
        private void OnTimerElapsed( object sender, ElapsedEventArgs e)
        {
            if (e.SignalTime < StopTime)
            {
                ServiceSubject subject = OracleServiceManager.Instance.Subject;
                ServiceController service = new ServiceController(
                            subject.Controller.ServiceName, subject.ServerName);
                ServiceContext context = new ServiceContext( service);
    
                if (State.GetType() != context.State.GetType())
                {
                    object[] stateArray = new object[] { context.State };
                    if (SynchronizingObject != null)
                      SynchronizingObject.BeginInvoke( changeStateCallback, 
                                                                stateArray);
                    else
                      ChangeState( context.State);
                }
            }
        }
  3. Similar to the DialogObserver, the TimerObserver also subscribes to all three (3) events. There are no surprises here. The code is short and almost self explanatory. The timer is stopped when the server changes, and starts when a new service is selected. The state is also recorded when any event occurs. The recorded state is used as a comparison to the current state in the Elapsed event above.
        private void OnServerChanged( object sender, EventArgs e)
        {
            ServiceSubject subject = (ServiceSubject) sender;
            StopTimer();
            State = subject.State;
        }
    
        private void OnServiceChanged( object sender, EventArgs e)
        {
            ServiceSubject subject = (ServiceSubject) sender;
            State = subject.State;
            StopTimer();
            if (subject.Controller != null)
                StartTimer();
        }
    
        private void OnStateChanged( object sender, EventArgs e)
        {
            ServiceSubject subject = (ServiceSubject) sender;
            State = subject.State;
        }

Resources

OsmResources provides methods needed to obtain localized icon and string resources. The identifiers for the resources have a particular naming convention to assist in retrieving the appropriate resources. I have not localized the application as yet, but future updates will include some localizations. I hope to translate to Spanish. Any volunteers for French, German, Italian, or Russian? If you take the time to localize the string resources (there're about 40 of them), then I will compile and include them in the distribution.

The OsmResources class is straightforward. It makes use of a common naming convention to retrieve the requested icon or string resource. The class also uses the System.Resources.ResourceManager to get the wonderful default behavior the .NET Framework provides. This default behavior allows for graceful degradation when the requested resource is not available in the satellite DLL. For more information, take a look at MSDN for how .NET handles globalization and localization.

Configuration & Options

My thanks to Nick Parker for extending the default read-only behavior of .NET configuration support. I used Nick’s Configuration class to enable read-write support for the OSM configuration file. I wanted to make use of the default .NET support for configuration classes, and was saddened when I learned they were read-only. The Configuration class stores the values managed in the options dialog and used by the application for polling frequency and action verification. Not a very interesting job, but someone’s gotta do it – and this class does it nicely. Thanks Nick.

Future Directions

Like all projects, there is never really an end – only an end to the beginning. And so it is with this project. I kept the project simple even when I noticed obvious areas for improvement or great enhancement suggestions. I have kept a list below of enhancements and improvement areas. I’m not sure when I’ll get around to incorporating these ideas…perhaps one rainy Sunday afternoon.

  1. Adapt OSM to work for multiple kinds of Windows databases including SQL Server, DB2, Sybase, MySQL, etc.
  2. Create a toolbar observer and package it in a Visual Studio add-in.
  3. Add a test bench – which I wanted to do but didn’t -- there’s always next project for test-first/test-driven design. BTW, I’m still looking for an example of how test first design, or test driven development, is applied to GUIs.
  4. Create localized resources for Spanish, French, German, and others.

Special Thanks

This project, while simple, has had its share of contributors – people who for a little bribery, like a drink, or maybe a lunch, provided invaluable help. Jason Pugh and Rick Barraza have provided me with guidance and support. Both have helped me test the solution, keep the project simple, and made great suggestions for the future noted above. Rick has lent his creative hand to manufacturing the icons – not normally a pixel pusher, but rather vector oriented, I was grateful to have his helping hand [and eye] – check out Rick’s site.

History

  • 14 May 2004 - Initial release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Rob Kayman

United States United States
Dwight D. Eisenhower wrote: "In battle, I believe plans are useless; however, planning is indespensable."
 
Hmm, perhaps he was an early adopter of agile beliefs?! Wink | ;-)

Comments and Discussions

 
GeneralI have a problemes in Database PinmemberRahad Rahman12-Oct-09 0:29 
GeneralMissing Files PinmemberLawrenceMarsh24-Jan-06 20:44 
GeneralRe: Missing Files Pinmemberhayj114-Jun-06 11:56 
GeneralRuntime Error PinmemberDimondWolfe28-Nov-05 11:13 
GeneralRe: Runtime Error PinmemberDimondWolfe28-Nov-05 11:25 
GeneralUnable to restart machine PinmemberIftach Smith14-Sep-05 0:17 
GeneralBug. PinmemberDanArnold@CodeRocks14-Aug-05 19:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.141015.1 | Last Updated 14 May 2004
Article Copyright 2004 by Rob Kayman
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid