Mittwoch, 19. Mai 2010

ProzessIDs lesen und beenden / Reading and terminating process IDs (Revisited)

Im letzten Eintrag ging es um das gezielte Beenden von Applikationen. In diesem Posting stelle ich nun eine kleine Abwandlung dieser Vorgehensweise vor. Genauer gesagt für die Aufgabenstellung, dass unsere soeben gestartete Applikation sicherstellen soll, das keine weiteren (zuvor gestarteten) Prozesse von ihr sich im Arbeitsspeicher befinden.

Diese Aufgabe läßt sich mit der Funktion TerminateProcess() des letzten Postings nicht durchführen, da dort die eigene ProzessID nicht verfügbar gemacht wurde.

Die eigene ProzessID herauszufinden ist in VFP nicht weiter schwierig. Das Applikationsobjekt _VFP stellt sie uns als eine seiner Eigenschaften zu Verfügung (_VFP.ProcessID). Die folgende Beispielfunktion prüft nun vor dem Terminierungsvorgang ab, ob der Zielprozess über die selbe ProzessID verfügt wie die aktuelle Applikation. Ist dies der Fall so wird die Terminierung nicht durchgeführt. Eine weitere Sicherheitsüberprüfung ist, ob der ausführende User auch der 'Eigentümer' der zu löschenden Prozesse ist (Diese Vorgehensweise ist analog zum vorherigen Posting).

* // Funktionstest
RUN /n vfp9.exe
RUN /n vfp9.exe

CLEAR 
? [beendete Prozesse:]
?? TerminateOldProcess( [vfp9.exe] )



FUNCTION TerminateOldProcess as Integer 
LPARAMETERS vAppname as String
    * // Funktion zum Löschen gleichnamiger Prozesse übergebener Programme    
    * // oder der aktuellen Applikation                                        
    * //                                                                    
    * // Parameter    Variable    Status                                        
    * // #1           vAppname    optional (default = Aktuelles Programm)        
    * //                          Name der Programmdatei                        
    vAppname = EVL( vAppname , PROGRAM() )

    * // Deklaration und Belegung benötigter Arbeitsvariablen. Hierbei        
    * // erfolgt bei den zwei Objekt-Variablen eine direkte Referenzierung    
    * // auf das WMI-Objekt sowie das Abfrageergebnis                        
    LOCAL    liReturn as Integer, llExit as Boolean, ;
            lcLogname as String , lcComputer as String, ;
            loCIMV2 as Object, loProcCols as Object, lcOwner as String
    liReturn    = 0
    llExit      = .F.
    lcLogname   = ALLTRIM( GETWORDNUM( SYS( 0 ) , 2 , [#] ) )
    lcComputer  = [.]

    TRY 
        loCIMV2    = GETOBJECT( [winmgmts:{impersonationLevel=impersonate}!\\] + lcComputer + [\root\cimv2] )
        loProcCols = loCIMV2.ExecQuery( [select * from Win32_Process where name='] + vAppname + ['] )
    CATCH 
        llExit     = .T.
    ENDTRY 
    
    * // Wenn die Instanziierungen erfolgreich waren, dann    
    * // kann es jetzt losgehen...                            
    IF !llExit

        * // Die gefundenen Prozesse entsorgen                
        FOR EACH objProcess in loProcCols

            * // Sicherstellen, dass nur eigene             
            * // Prozesse gelöscht werden!                    
            lcOwner = SPACE( 256 )
            = objProcess.GetOwner( @lcOwner )

            IF  lcLogname == lcOwner ;
            AND _VFP.ProcessID <> objProcess.ProcessID
                liReturn = liReturn + 1 
                objProcess.Terminate( 0 )
            ENDIF 

        ENDFOR 
        
        * // WMI-Objektreferenzen auflösen                     
        loCIMV2    = .NULL.
        loProcCols = .NULL.
        
    ENDIF 
    
    * // Anzahl der gelöschten Prozesse zurückgeben.        
    RETURN liReturn 
    
ENDFUNC 

Montag, 10. Mai 2010

ProzessIDs lesen und beenden / Reading and terminating process IDs

Wenn wir, aus welchem Grund auch immer, sicherstellen wollen, das eine bestimmte Anwendung nur einmal gestartet sein darf, dann kommt üblicherweise ein Singleton-Pattern zum Einsatz. Bei der Arbeit mit Objekten ist dies für viele sicherlich eine immer wiederkehrende Routine und die Funktion PEMSTATUS() wird in diesem Fall wohl auch oft genug zum Einsatz kommen.

Wollen wir jedoch eine komplette Applikation aus dem Arbeitsspeicher entfernen oder einfach nur überprüfen, ob eine Anwendung bereits aktiv ist, dann können wir diese Prüfung nicht mit PEMSTATUS() durchführen.

Bereits im April und Dezember habe ich in meinen Postings zur Druckerstatus Abfrage die Windows Management Instrumentation eingesetzt. Jetzt kommt sie erneut zum Einsatz, um uns Prozessinformationen zu liefern.

Die unten stehende Demofunktion 'TerminateProcess()' dient dem gezielten Entsorgen von unerwünschten Prozessen. Im Bereich der Singleton Patterns ist dies nicht immer die gewünschte Vorgehensweise. Aus diesem Grund verfügt die Funktion auch über Parameter, mit denen gezielte Abfragen und Ansichten ohne Prozesslöschung durchgeführt werden können.

Wird bspw. als einziger Parameter der Programmname übergeben erfolgt eine automatische Löschung sämtlicher gefunden Prozesse dieser Applikation. Die beiden Funktionstests zeigen mögliche unterschiedliche Parametrisierungen auf.

* // Funktionstest 1
* // - Anzahl vorhandener Prozesse zurückmelden
RUN /n notepad.exe
RUN /n notepad.exe
RUN /n notepad.exe

liAnzahl = TerminateProcess( [notepad.exe] , .T. )

CLEAR 
? [gefundene Prozesse: ]
?? liAnzahl

* // Funktionstest 2
* // - alle gefundenen Prozesse entsorgen
* // Alternativer Aufruf um gefundene Prozesse anzuzeigen
* liAnzahl = TerminateProcess( [notepad.exe] , .F. , .F. , .T. )
liAnzahl = TerminateProcess( [notepad.exe] )
 
? [beendete Prozesse:  ]
?? liAnzahl

FUNCTION TerminateProcess as Integer 
LPARAMETERS vAppname as String, vJustCheck as Boolean, vAllButLast as Boolean, vBrowseLast as Boolean
    * // Funktion zum löschen/melden von Prozessen übergebener Programmnamen
    * //
    * // Parameter    Variable    Status
    * // #1           vAppname    optional (default = Aktuelles Programm)
    * //                          Name der Programmdatei
    * // #2           vJustCheck  optional (default = .F.)
    * //                          Nicht löschen, nur Melden
    * // #3           vAllButLast optional (default = .F.)
    * //                          Letzen Prozess nicht löschen
    * // #4           vBrowseLast optional (default = .F.)
    * //                          Gefundene Prozesse anzeigen
    vAppname    = EVL( vAppname , PROGRAM() )
    vAllButLast = EVL( vAllButLast , .F. )
    vBrowseLast = EVL( vBrowseLast , .F. )

    * // Deklaration und Belegung benötigter Arbeitsvariablen. Hierbei
    * // erfolgt bei den zwei Objekt-Variablen eine direkte Referenzierung
    * // auf das WMI-Objekt sowie das Abfrageergebnis
    LOCAL   liReturn as Integer, liCount as Integer , llExit as Boolean, ;
            lcLogname as String , lcComputer as String, ;
            loCIMV2 as Object, loProcCols as Object, lcOwner as String
    liReturn    = 0
    liCount     = 0
    llExit      = .F.
    lcLogname   = ALLTRIM( GETWORDNUM( SYS( 0 ) , 2 , [#] ) )
    lcComputer  = [.]

    TRY 
        loCIMV2    = GETOBJECT( [winmgmts:{impersonationLevel=impersonate}!\\] + lcComputer + [\root\cimv2] )
        loProcCols = loCIMV2.ExecQuery( [select * from Win32_Process where name='] + vAppname + ['] )
    CATCH 
        llExit     = .T.
    ENDTRY 
    
    IF !llExit
        * // Arbeitscursor erstellen und die WMI Objekte verarbeiten
        CREATE CURSOR crsTasks ( ProgOwner c( 30 ) , ProgName c( 30 ) , ProgPath c( 200 ) )
        FOR EACH objProcess in loProcCols
            liCount     = liCount + 1 
            lcOwner     = SPACE( 256 )
            = objProcess.GetOwner( @lcOwner )
            m.ProgOwner = lcOwner
            m.ProgName  = objProcess.Name
            m.ProgPath  = EVL( objProcess.ExecutablePath , [-] )
            INSERT INTO crsTasks FROM MEMVAR 
        ENDFOR 

        * // Ggf. die gefundenen Prozesse angezeigen (Parameter #4)
        IF vBrowseLast
            BROWSE LAST 
        ENDIF 

        IF !vJustCheck
            * // Die gefundenen Prozesse der Reihe nach entsorgen
            FOR EACH objProcess in loProcCols
                liReturn = liReturn + 1 
                * // Wenn der letzte Prozess beibehalten werden soll (Parm.#3)
                * // dann raus aus der Schleife, andernfalls geht's weiter bis
                * // zum bitteren Ende... ;-)
                IF vAllButLast AND liReturn = liCount 
                    EXIT 
                ELSE 
                    SELECT crsTasks
                    GO ( liReturn )
                    IF ALLTRIM( crsTasks.ProgOwner ) == lcLogname
                        objProcess.Terminate( 0 )
                    ENDIF 
                ENDIF 
            ENDFOR 
        ELSE 
            liReturn = liCount
        ENDIF 
        
        * // Arbeitscursor entsorgen, WMI-Objektreferenzen auflösen
        USE IN SELECT( [crsTasks] )
        loCIMV2    = .NULL.
        loProcCols = .NULL.
        
    ENDIF 
    
    * // Anzahl der gelöschten/gefundenen Prozesse zurückgeben
    RETURN liReturn 
    
ENDFUNC 

Weitere Aufrufmöglichkeiten:

Löscht alle Prozesse des übergebenen Programms, der letzte Prozess bleibt jedoch bestehen

liAnzahl = TerminateProcess( [notepad.exe] , .F. , .T. )

Löscht alle Prozesse des übergebenen Programms, zeigt jedocht zuvor eine Liste der gefundenen Prozesse an

liAnzahl = TerminateProcess( [notepad.exe] , .F. , .F. , .T. )

Dienstag, 4. Mai 2010

Inaktivität des Anwenders über TIMEOUT prüfen / Using TIMEOUT to check user inactivity

Eine unangenehme Eigenschaft von VFP ist, dass eine Applikation, die aus einer EXE heraus gestartet wurde, solange für Updatevorgänge gesperrt bleibt, bis die aufrufende EXE beendet wird.

Jetzt stellt dies nicht wirklich ein Problem dar, denn üblicherweise genügt es, den Anwender zu informieren, das Programm für einen Updatevorgang zu beenden.

Dieser Gedankengang hat allerdings einen kleinen Haken, den Murphy sich nur zu gerne zu Nutzen macht. In einigen Fällen ist der Anwender, der eine Applikation sperrt, gerade nicht an seinem Arbeitsplatz...

Damit unsere Anwendung von sich aus feststellen kann, ob der Anwender eine 'Denkpause' eingelegt hat, benötigen wir eine systemweite Prüfung auf Aktivitäten bzw. in unserem Fall auf Inaktivitäten. Ist bspw. über einen Zeitraum von 15 Minuten keine Benutzereingabe erfolgt, dann können wir mit unserem Programm reagieren, und eine zeitgesteuerte Messagebox einblenden. Welche wiederum nach Ablauf ihres Timeouts ggf. das automatische und geordnete Beenden unserer Anwendung durchführen kann.

Im u.a. Beispiel wird mit einer PUBLIC Variablen gearbeitet. Alternativ kann das Objekt auch in einer Applikationsproperty erzeugt werden. Wichtig ist auf jeden Fall, dass der Timer jederzeit erreichbar ist.

Der Timer kann mit zwei Parametern versehen werden.
Parameter 1 definiert den Timeout Zeitraum in Minuten
Parameter 2 gibt den Timerzyklus in Sekunden vor

Wer die Parameter nicht nutzen möchte kann natürlich die entsprechenden Eigenschaften sozusagen 'ab Werk' vorbesetzen.

* // Funktionstext                                                    
CLEAR 
PUBLIC goTimer as Timer
* // 1 minütiger Timeout mit 15 Sekunden Prüfinterval                
* // Zum Beenden im Befehlsfenster 'Release goTimer' eingeben        
goTimer = CREATEOBJECT( [InactivityTimer] , 1 , 15 )

* // Bemerkt Benutzeraktivitäten und feuert ein Ereignis, nachdem    
* // der definierte Zeitraum für Inaktivität überschritten wurde.    
DEFINE CLASS InactivityTimer as Timer 

    * // Deklaration der API Konstanten                                
    #DEFINE WM_KEYUP        0x0101
    #DEFINE WM_SYSKEYUP     0x0105
    #DEFINE WM_MOUSEMOVE    0x0200
    #DEFINE GWL_WNDPROC     (-4)

    * // Interne Eigenschaften setzen und Timer setzen (5Sek.)        
    _iTimeoutInMinutes      = 0
    _tLastActivity          = {/:}
    Interval                = 5000
    Enabled                 = .T.

    * // Auf API Ereignisse horchen sobald die Form gestartet wurde    
    * // Optional wird ein Timeout Wert als Parameter übergeben        
    * // Zusätzlich kann als weiterer Parameter der Prüfinterval in    
    * // Sekunden übergeben werden.                                    
    PROCEDURE Init ( vTimeoutInMinutes as Integer , vIntervalInSeconds as Integer )

        WITH This
            ._iTimeoutInMinutes  = EVL( vTimeoutInMinutes , 1 )
            .Interval            = EVL( vIntervalInSeconds , 5 ) * 1000
            ._tLastActivity      = DATETIME()
        ENDWITH 
        BINDEVENT( 0 , WM_KEYUP ,     This , [WndProc] )
        BINDEVENT( 0 , WM_SYSKEYUP ,  This , [WndProc] )
        BINDEVENT( 0 , WM_MOUSEMOVE , This , [WndProc] )

    ENDPROC 

    * // mit dem Lauschen aufhören                                    
    PROCEDURE Unload 

        UNBINDEVENTS( 0 , WM_KEYUP )
        UNBINDEVENTS( 0 , WM_SYSKEYUP )
        UNBINDEVENTS( 0 , WM_MOUSEMOVE )

    ENDPROC 

    * // Jedes Ereignis zählt als Aktivität...                        
    PROCEDURE WndProc ( hWnd as Long, Msg as Long, wParam as Long, lParam as Long )

        This._tLastActivity = DATETIME()

    ENDPROC 

    * // Letzte Aktivität mit Timeout abgleichen                    
    PROCEDURE Timer

        WITH This
            LOCAL ltFireEvent as Datetime 
            ltFireEvent = ._tLastActivity + ( 60 * ._iTimeoutInMinutes )
            IF DATETIME() >= ltFireEvent
                .eventTimeout()
            ENDIF
        ENDWITH  

    ENDPROC 

    * // Diese Methode über BINDEVENT übersteuern oder enthaltenen    
    * // Code überschreiben...                                        
    * // Bspw. kann hier der Wert von iTimeoutInMinutes überschrie-    
    * // ben werden um einen mehrstufigen Timeout zu ermöglichen    
    PROCEDURE eventTimeout

        MESSAGEBOX( [<<< TIMEOUT >>>] , 0 , [Inaktiv] )

    ENDPROC 

ENDDEFINE