(file) Return to Thread.cpp CVS log (file) (dir) Up to [Pegasus] / pegasus / src / Pegasus / Common

  1 karl  1.89 //%2006////////////////////////////////////////////////////////////////////////
  2 mike  1.2  //
  3 karl  1.71 // Copyright (c) 2000, 2001, 2002 BMC Software; Hewlett-Packard Development
  4            // Company, L.P.; IBM Corp.; The Open Group; Tivoli Systems.
  5            // Copyright (c) 2003 BMC Software; Hewlett-Packard Development Company, L.P.;
  6 karl  1.56 // IBM Corp.; EMC Corporation, The Open Group.
  7 karl  1.71 // Copyright (c) 2004 BMC Software; Hewlett-Packard Development Company, L.P.;
  8            // IBM Corp.; EMC Corporation; VERITAS Software Corporation; The Open Group.
  9 karl  1.75 // Copyright (c) 2005 Hewlett-Packard Development Company, L.P.; IBM Corp.;
 10            // EMC Corporation; VERITAS Software Corporation; The Open Group.
 11 karl  1.89 // Copyright (c) 2006 Hewlett-Packard Development Company, L.P.; IBM Corp.;
 12            // EMC Corporation; Symantec Corporation; The Open Group.
 13 mike  1.2  //
 14            // Permission is hereby granted, free of charge, to any person obtaining a copy
 15 chip  1.11 // of this software and associated documentation files (the "Software"), to
 16            // deal in the Software without restriction, including without limitation the
 17            // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 18 mike  1.2  // sell copies of the Software, and to permit persons to whom the Software is
 19            // furnished to do so, subject to the following conditions:
 20 karl  1.89 // 
 21 chip  1.11 // THE ABOVE COPYRIGHT NOTICE AND THIS PERMISSION NOTICE SHALL BE INCLUDED IN
 22 mike  1.2  // ALL COPIES OR SUBSTANTIAL PORTIONS OF THE SOFTWARE. THE SOFTWARE IS PROVIDED
 23            // "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
 24 chip  1.11 // LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 25            // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 26            // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 27 mike  1.2  // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 28            // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 29            //
 30            //==============================================================================
 31            //
 32            // Author: Mike Day (mdday@us.ibm.com)
 33            //
 34            // Modified By: Rudy Schuet (rudy.schuet@compaq.com) 11/12/01
 35 chip  1.11 //              added nsk platform support
 36 kumpf 1.59 //              Roger Kumpf, Hewlett-Packard Company (roger_kumpf@hp.com)
 37 a.arora 1.64 //              Amit K Arora, IBM (amita@in.ibm.com) for PEP#101
 38 gs.keenan 1.76 //              Sean Keenan, Hewlett-Packard Company (sean.keenan@hp.com)
 39 david.dillard 1.83 //              David Dillard, VERITAS Software Corp.
 40                    //                  (david.dillard@veritas.com)
 41 mike          1.2  //
 42                    //%/////////////////////////////////////////////////////////////////////////////
 43                    
 44                    #include "Thread.h"
 45 kumpf         1.68 #include <exception>
 46 kumpf         1.14 #include <Pegasus/Common/Tracer.h>
 47 mike          1.91 #include "Time.h"
 48 mike          1.2  
 49 mike          1.91 PEGASUS_USING_STD;
 50 mike          1.2  
 51                    PEGASUS_NAMESPACE_BEGIN
 52                    
 53 mike          1.91 //==============================================================================
 54                    //
 55                    // POSIX Threads Implementation:
 56                    //
 57                    //==============================================================================
 58 mday          1.42 
 59 mike          1.91 #if defined(PEGASUS_HAVE_PTHREADS)
 60                    
 61                    struct StartWrapperArg
 62 chip          1.11 {
 63 mike          1.91     void *(PEGASUS_THREAD_CDECL * start) (void *);
 64                        void *arg;
 65                    };
 66 mike          1.2  
 67 mike          1.91 extern "C" void *_start_wrapper(void *arg_)
 68 chuck         1.43 {
 69 mike          1.91     StartWrapperArg *arg = (StartWrapperArg *) arg_;
 70 chuck         1.43 
 71 mike          1.91     void *return_value = (*arg->start) (arg->arg);
 72                        delete arg;
 73 chuck         1.37 
 74 mike          1.91     return return_value;
 75                    }
 76 mike          1.2  
 77 mike          1.91 void Thread::cancel()
 78 mike          1.2  {
 79 mike          1.91     _cancelled = true;
 80                        pthread_cancel(_handle.thid.tt_handle());
 81 mike          1.2  }
 82 kumpf         1.81 
 83 mike          1.91 void Thread::test_cancel()
 84 mike          1.2  {
 85 mike          1.91 #if defined(PEGASUS_PLATFORM_ZOS_ZSERIES_IBM)
 86                        pthread_testintr();
 87                    #else
 88                        pthread_testcancel();
 89                    #endif
 90 mike          1.2  }
 91 kumpf         1.81 
 92 mike          1.91 Boolean Thread::is_cancelled(void)
 93 chip          1.11 {
 94 mike          1.91     return _cancelled;
 95 mike          1.2  }
 96                    
 97 mike          1.91 void Thread::thread_switch()
 98                    {
 99                    #if defined(PEGASUS_PLATFORM_ZOS_ZSERIES_IBM)
100                        pthread_yield(NULL);
101                    #else
102                        sched_yield();
103 mike          1.2  #endif
104 mike          1.91 }
105 mike          1.2  
106 mike          1.91 /*
107                    ATTN: why are these missing on other platforms?
108                    */
109                    #if defined(PEGASUS_PLATFORM_LINUX_GENERIC_GNU)
110                    void Thread::suspend()
111 chuck         1.39 {
112 mike          1.91     pthread_kill(_handle.thid.tt_handle(), SIGSTOP);
113 chuck         1.39 }
114                    
115 mike          1.91 void Thread::resume()
116 chuck         1.37 {
117 mike          1.91     pthread_kill(_handle.thid.tt_handle(), SIGCONT);
118 chuck         1.39 }
119 mike          1.91 #endif
120 chuck         1.39 
121 mike          1.91 void Thread::sleep(Uint32 msec)
122 chuck         1.39 {
123 mike          1.91     Threads::sleep(msec);
124 chuck         1.37 }
125                    
126 mike          1.91 void Thread::join(void)
127 chuck         1.37 {
128 mike          1.91     if (!_is_detached && Threads::id(_handle.thid) != 0)
129                            pthread_join(_handle.thid.tt_handle(), &_exit_code);
130 kumpf         1.81 
131 mike          1.91     Threads::clear(_handle.thid);
132 chuck         1.37 }
133                    
134 mike          1.91 void Thread::thread_init(void)
135 chuck         1.37 {
136 mike          1.91 #if defined(PEGASUS_PLATFORM_ZOS_ZSERIES_IBM)
137                        pthread_setintr(PTHREAD_INTR_ENABLE);
138                        pthread_setintrtype(PTHREAD_INTR_ASYNCHRONOUS);
139                    #else
140                        pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
141                        pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL);
142                    #endif
143                        _cancel_enabled = true;
144 chuck         1.37 }
145                    
146 mike          1.91 void Thread::detach(void)
147 chuck         1.37 {
148 mike          1.91     _is_detached = true;
149                    #if defined(PEGASUS_PLATFORM_ZOS_ZSERIES_IBM)
150                        pthread_t  thread_id=_handle.thid.tt_handle();
151                        pthread_detach(&thread_id);
152                    #else
153                        pthread_detach(_handle.thid.tt_handle());
154                    #endif
155 chuck         1.37 }
156                    
157 mike          1.91 ThreadStatus Thread::run()
158                    {
159                        StartWrapperArg *arg = new StartWrapperArg;
160                        arg->start = _start;
161                        arg->arg = this;
162 mday          1.52 
163 mike          1.91     Threads::Type type = _is_detached ? Threads::DETACHED : Threads::JOINABLE;
164                        int rc = Threads::create(_handle.thid, type, _start_wrapper, arg);
165 kumpf         1.81 
166 mike          1.91     // On Linux distributions released prior 2005, the implementation of 
167                        // Native POSIX Thread Library returns ENOMEM instead of EAGAIN when
168                        // there 
169                        // are no insufficient memory.  Hence we are checking for both.  See bug
170                        // 386.
171 mday          1.58 
172 mike          1.91     if ((rc == EAGAIN) || (rc == ENOMEM))
173 kumpf         1.81     {
174 mike          1.91         Threads::clear(_handle.thid);
175                            delete arg;
176                            return PEGASUS_THREAD_INSUFFICIENT_RESOURCES;
177 kumpf         1.81     }
178 mike          1.91     else if (rc != 0)
179 kumpf         1.81     {
180 mike          1.91         Threads::clear(_handle.thid);
181                            delete arg;
182                            return PEGASUS_THREAD_SETUP_FAILURE;
183 kumpf         1.81     }
184 mike          1.91     return PEGASUS_THREAD_OK;
185                    }
186 mday          1.58 
187 mike          1.91 static sigset_t *block_signal_mask(sigset_t * sig)
188                    {
189                        sigemptyset(sig);
190                        // should not be used for main()
191                        sigaddset(sig, SIGHUP);
192                        sigaddset(sig, SIGINT);
193                        // maybe useless, since KILL can't be blocked according to POSIX
194                        sigaddset(sig, SIGKILL);
195                    
196                        sigaddset(sig, SIGABRT);
197                        sigaddset(sig, SIGALRM);
198                        sigaddset(sig, SIGPIPE);
199                    
200                    
201                    // Note: older versions of the linux pthreads library use SIGUSR1 and SIGUSR2
202                    // internally to stop and start threads that are blocking, the newer ones
203                    // implement this through the kernel's real time signals
204                    // since SIGSTOP/CONT can handle suspend()/resume() on Linux
205                    // block them
206                    // #if defined(PEGASUS_PLATFORM_LINUX_IX86_GNU)
207                    //     sigaddset(sig, SIGUSR1);
208 mike          1.91 //     sigaddset(sig, SIGUSR2);
209                    // #endif
210                    #ifndef PEGASUS_PLATFORM_ZOS_ZSERIES_IBM
211                        pthread_sigmask(SIG_BLOCK, sig, NULL);
212                    #else
213                        sigprocmask(SIG_BLOCK, sig, NULL);
214                    #endif
215                        return sig;
216                    }
217 mday          1.52 
218 mike          1.91 Thread::Thread(ThreadReturnType(PEGASUS_THREAD_CDECL * start) (void *), void *parameter, Boolean detached):_is_detached(detached),
219                    _cancel_enabled(true),
220                    _cancelled(false),
221                    _start(start), _cleanup(), _tsd(), _thread_parm(parameter), _exit_code(0)
222                    {
223                        Threads::clear(_handle.thid);
224 kumpf         1.81 }
225 mday          1.20 
226 mike          1.91 Thread::~Thread()
227 mday          1.20 {
228 kumpf         1.81     try
229                        {
230 mike          1.91         join();
231                            empty_tsd();
232 kumpf         1.81     }
233 mike          1.91     catch(...)
234 kumpf         1.81     {
235 mike          1.91         // Do not allow the destructor to throw an exception
236 kumpf         1.81     }
237 mday          1.20 }
238                    
239 mike          1.91 #endif /* PEGASUS_HAVE_PTHREADS */
240                    
241                    //==============================================================================
242                    //
243                    // Windows Threads Implementation:
244                    //
245                    //==============================================================================
246 kumpf         1.81 
247 mike          1.91 #if defined(PEGASUS_HAVE_WINDOWS_THREADS)
248 kumpf         1.81 
249 mike          1.91 ThreadStatus Thread::run(void)
250                    {
251                        // Note: A Win32 thread ID is not the same thing as a pthread ID.
252                        // Win32 threads have both a thread ID and a handle.  The handle
253                        // is used in the wait functions, etc.
254                        // So _handle.thid is actually the thread handle.
255 kumpf         1.81 
256 mike          1.91     unsigned threadid = 0;
257 mike          1.2  
258 mike          1.91     ThreadType tt;
259                        tt.handle = (HANDLE) _beginthreadex(NULL, 0, _start, this, 0, &threadid);
260                        _handle.thid = tt;
261 chuck         1.39 
262 mike          1.91     if (Threads::id(_handle.thid) == 0)
263                        {
264                            if (errno == EAGAIN)
265 kumpf         1.81         {
266 mike          1.91             return PEGASUS_THREAD_INSUFFICIENT_RESOURCES;
267 kumpf         1.81         }
268 mike          1.91         else
269 kumpf         1.81         {
270 mike          1.91             return PEGASUS_THREAD_SETUP_FAILURE;
271 kumpf         1.81         }
272 mike          1.91     }
273                        return PEGASUS_THREAD_OK;
274                    }
275 mday          1.52 
276 mike          1.91 void Thread::cancel(void)
277                    {
278                        _cancelled = true;
279                    }
280 kumpf         1.82 
281 mike          1.91 void Thread::test_cancel(void)
282                    {
283                        if (_cancel_enabled && _cancelled)
284                        {
285                            exit_self(0);
286                        }
287                    }
288 kumpf         1.82 
289 mike          1.91 Boolean Thread::is_cancelled(void)
290                    {
291                        return _cancelled;
292                    }
293 kumpf         1.82 
294 mike          1.91 void Thread::thread_switch(void)
295                    {
296                        Sleep(0);
297                    }
298 kumpf         1.82 
299 mike          1.91 void Thread::sleep(Uint32 milliseconds)
300                    {
301                        Sleep(milliseconds);
302                    }
303 mike          1.2  
304 mike          1.91 void Thread::join(void)
305                    {
306                        if (Threads::id(_handle.thid) != 0)
307                        {
308                            if (!_is_detached)
309                            {
310                                if (!_cancelled)
311 kumpf         1.82             {
312 mike          1.91                 // Emulate the unix join api. Caller sleeps until thread is
313                                    // done.
314                                    WaitForSingleObject(_handle.thid.handle, INFINITE);
315 kumpf         1.82             }
316 mike          1.91             else
317 kumpf         1.82             {
318 mike          1.91                 // Currently this is the only way to ensure this code does
319                                    // not 
320                                    // hang forever.
321                                    if (WaitForSingleObject(_handle.thid.handle, 10000) ==
322                                        WAIT_TIMEOUT)
323 kumpf         1.82                 {
324 mike          1.91                     TerminateThread(_handle.thid.handle, 0);
325 kumpf         1.82                 }
326 mike          1.91             }
327 s.hills       1.49 
328 mike          1.91             DWORD exit_code = 0;
329                                GetExitCodeThread(_handle.thid.handle, &exit_code);
330                                _exit_code = (ThreadReturnType) exit_code;
331 kumpf         1.81         }
332 mike          1.91 
333                            CloseHandle(_handle.thid.handle);
334                            Threads::clear(_handle.thid);
335 kumpf         1.82     }
336 mike          1.91 }
337 kumpf         1.14 
338 mike          1.91 void Thread::thread_init(void)
339                    {
340                        _cancel_enabled = true;
341 mike          1.2  }
342                    
343 mike          1.91 void Thread::detach(void)
344 mike          1.2  {
345 mike          1.91     _is_detached = true;
346                    }
347 kumpf         1.81 
348 mike          1.91 Thread::Thread(ThreadReturnType(PEGASUS_THREAD_CDECL * start) (void *),
349                                   void *parameter,
350                                   Boolean detached):_is_detached(detached),
351                    _cancel_enabled(true),
352                    _cancelled(false),
353                    _start(start), _cleanup(), _tsd(), _thread_parm(parameter), _exit_code(0)
354                    {
355                        Threads::clear(_handle.thid);
356                    }
357 kumpf         1.81 
358 mike          1.91 Thread::~Thread()
359                    {
360 kumpf         1.81     try
361                        {
362 mike          1.91         join();
363                            empty_tsd();
364                        }
365                        catch(...)
366                        {
367                        }
368                    }
369 kumpf         1.81 
370 mike          1.91 #endif /* PEGASUS_HAVE_WINDOWS_THREADS */
371 kumpf         1.57 
372 mike          1.91 //==============================================================================
373                    //
374                    // Common implementation:
375                    //
376                    //==============================================================================
377 kumpf         1.81 
378 mike          1.91 void thread_data::default_delete(void *data)
379                    {
380                        if (data != NULL)
381                            ::operator  delete(data);
382                    }
383 mike          1.2  
384 mike          1.91 void language_delete(void *data)
385                    {
386                        if (data != NULL)
387                        {
388                            AutoPtr < AcceptLanguageList > al(static_cast <
389                                                              AcceptLanguageList * >(data));
390 kumpf         1.57     }
391 mike          1.2  }
392                    
393 mike          1.91 Boolean Thread::_signals_blocked = false;
394                    #ifndef PEGASUS_OS_ZOS
395                    TSDKeyType Thread::_platform_thread_key = TSDKeyType(-1);
396                    #else
397                    TSDKeyType Thread::_platform_thread_key;
398                    #endif
399                    Boolean Thread::_key_initialized = false;
400                    Boolean Thread::_key_error = false;
401 mday          1.12 
402 mike          1.91 void Thread::cleanup_push(void (*routine) (void *), void *parm)
403 mike          1.2  {
404 mike          1.91     AutoPtr < cleanup_handler > cu(new cleanup_handler(routine, parm));
405                        _cleanup.insert_front(cu.get());
406                        cu.release();
407                        return;
408                    }
409 kumpf         1.81 
410 mike          1.91 void Thread::cleanup_pop(Boolean execute)
411                    {
412                        AutoPtr < cleanup_handler > cu;
413                        try
414                        {
415                            cu.reset(_cleanup.remove_front());
416                        }
417                        catch(IPCException &)
418                        {
419                            PEGASUS_ASSERT(0);
420                        }
421                        if (execute == true)
422                            cu->execute();
423                    }
424 kumpf         1.81 
425                    
426 mike          1.91 //thread_data *Thread::put_tsd(const Sint8 *key, void (*delete_func)(void *), Uint32 size, void *value)
427 kumpf         1.81 
428                    
429 mike          1.91 void Thread::exit_self(ThreadReturnType exit_code)
430                    {
431                    #if defined(PEGASUS_PLATFORM_HPUX_ACC) || \
432                        defined(PEGASUS_PLATFORM_LINUX_GENERIC_GNU)
433                        // NOTE: pthread_exit exhibits unusual behavior on RHEL 3 U2, as
434                        // documented in Bugzilla 3836.  Where feasible, it may be advantageous
435                        // to avoid using this function.
436                        pthread_exit(exit_code);
437                    #else
438                        // execute the cleanup stack and then return
439                        while (_cleanup.size())
440                        {
441 kumpf         1.81         try
442                            {
443 mike          1.91             cleanup_pop(true);
444 kumpf         1.81         }
445 mike          1.91         catch(IPCException &)
446 kumpf         1.81         {
447 mike          1.91             PEGASUS_ASSERT(0);
448 kumpf         1.81             break;
449                            }
450 mike          1.91     }
451                        _exit_code = exit_code;
452                        Threads::exit(exit_code);
453                        Threads::clear(_handle.thid);
454                    #endif
455                    }
456 kumpf         1.81 
457 mike          1.91 Sint8 Thread::initializeKey()
458                    {
459                        PEG_METHOD_ENTER(TRC_THREAD, "Thread::initializeKey");
460                        if (!Thread::_key_initialized)
461                        {
462                            if (Thread::_key_error)
463                            {
464                                Tracer::trace(TRC_THREAD, Tracer::LEVEL4,
465                                              "Thread: ERROR - thread key error");
466                                return -1;
467                            }
468 kumpf         1.81 
469 mike          1.91         if (TSDKey::create(&Thread::_platform_thread_key) == 0)
470 kumpf         1.81         {
471 mike          1.91             Tracer::trace(TRC_THREAD, Tracer::LEVEL4,
472                                              "Thread: able to create a thread key");
473                                Thread::_key_initialized = true;
474 kumpf         1.81         }
475                            else
476                            {
477 mike          1.91             Tracer::trace(TRC_THREAD, Tracer::LEVEL4,
478                                              "Thread: ERROR - unable to create a thread key");
479                                Thread::_key_error = true;
480                                return -1;
481 kumpf         1.81         }
482 konrad.r      1.67     }
483 kumpf         1.81 
484                        PEG_METHOD_EXIT();
485 mike          1.91     return 0;
486 konrad.r      1.67 }
487 mday          1.19 
488 mike          1.91 Thread *Thread::getCurrent()
489 mday          1.19 {
490 mike          1.91     PEG_METHOD_ENTER(TRC_THREAD, "Thread::getCurrent");
491                        if (Thread::initializeKey() != 0)
492                        {
493                            return NULL;
494                        }
495 kumpf         1.81     PEG_METHOD_EXIT();
496 mike          1.91     return (Thread *) TSDKey::get_thread_specific(_platform_thread_key);
497 mday          1.19 }
498                    
499 mike          1.91 void Thread::setCurrent(Thread * thrd)
500 mday          1.19 {
501 mike          1.91     PEG_METHOD_ENTER(TRC_THREAD, "Thread::setCurrent");
502                        if (Thread::initializeKey() == 0)
503 kumpf         1.81     {
504 mike          1.91         if (TSDKey::
505                                set_thread_specific(Thread::_platform_thread_key,
506                                                    (void *) thrd) == 0)
507                            {
508                                Tracer::trace(TRC_THREAD, Tracer::LEVEL4,
509                                    "Successful set Thread * into thread specific storage");
510                            }
511                            else
512                            {
513                                Tracer::trace(TRC_THREAD, Tracer::LEVEL4,
514                                    "ERROR: error setting Thread * into thread specific storage");
515                            }
516 kumpf         1.81     }
517 mike          1.91     PEG_METHOD_EXIT();
518 mday          1.19 }
519                    
520 mike          1.91 AcceptLanguageList *Thread::getLanguages()
521 mday          1.19 {
522 mike          1.91     PEG_METHOD_ENTER(TRC_THREAD, "Thread::getLanguages");
523                    
524                        Thread *curThrd = Thread::getCurrent();
525                        if (curThrd == NULL)
526                            return NULL;
527                        AcceptLanguageList *acceptLangs =
528                            (AcceptLanguageList *) curThrd->reference_tsd("acceptLanguages");
529                        curThrd->dereference_tsd();
530                        PEG_METHOD_EXIT();
531                        return acceptLangs;
532 mday          1.19 }
533                    
534 mike          1.91 void Thread::setLanguages(AcceptLanguageList * langs)   // l10n
535 mday          1.19 {
536 mike          1.91     PEG_METHOD_ENTER(TRC_THREAD, "Thread::setLanguages");
537 kumpf         1.81 
538 mike          1.91     Thread *currentThrd = Thread::getCurrent();
539                        if (currentThrd != NULL)
540 kumpf         1.81     {
541 mike          1.91         // deletes the old tsd and creates a new one
542                            currentThrd->put_tsd("acceptLanguages",
543                                                 language_delete,
544                                                 sizeof (AcceptLanguageList *), langs);
545 kumpf         1.81     }
546                    
547                        PEG_METHOD_EXIT();
548 mday          1.19 }
549 mike          1.2  
550 mike          1.91 void Thread::clearLanguages()   // l10n
551 kumpf         1.81 {
552 mike          1.91     PEG_METHOD_ENTER(TRC_THREAD, "Thread::clearLanguages");
553                    
554                        Thread *currentThrd = Thread::getCurrent();
555                        if (currentThrd != NULL)
556 kumpf         1.81     {
557 mike          1.91         // deletes the old tsd
558                            currentThrd->delete_tsd("acceptLanguages");
559 kumpf         1.81     }
560                    
561 mike          1.91     PEG_METHOD_EXIT();
562 kumpf         1.81 }
563 mike          1.2  
564 mike          1.91 // ATTN: not sure where to put this!
565                    #ifdef PEGASUS_ZOS_SECURITY
566                    bool isEnhancedSecurity = 99;
567                    #endif
568                    
569 mike          1.2  PEGASUS_NAMESPACE_END

No CVS admin address has been configured
Powered by
ViewCVS 0.9.2