Inactivity Auto Logout for Dynamics GP

Updated 18th May 2014 – Amended to improve stability

October 2015 - New post Inactivity Auto Logout for Dynamics GP  Part II GP2013R2 onwards adds extra considerations

Concurrent licences

Dynamics GP is licenced on a concurrent user model. Hitting max user count limit, yet having some of the user sessions idle for hours at the same time as the user is AFK (Away From Keyboard) is a problem for many installations. Asking politely for users to logout is not enough as users cannot always anticipate when they will be unexpectedly pulled away from their work, sometimes for extended times leaving GP logged in hogging a valuable licence.

image

A colleague thought one approach to address the issues may be to allow the users to click a button, see on the above screen his idea, in order to see the users that have been idle too long. Thus the users themselves get to do the leg work of phoning up those licence hogs to get them to logout. I have written this version, by hooking into the form that pops up for maximum user count. A better way to tackle this in my view is with an inactivity auto logout approach.

User licences are so expensive, so having them laying idle is not acceptable so I tried looking for an auto logout function in GP. Alas it is not there. Rockton Software have a GP Toolbox, which has a feature rich auto logout, go check it out. This looked to be part of a larger toolkit that I didn’t require.  Thus I rolled up my sleeves and fired up Visual Studio and Google.

image

Using SQL

I was really only targeting the occasional users, not so much core users and didn’t want to force people out during dangerous activities like posting or editing records. I toyed with using a SQL script and SQL Job to kill off users from the activity table. I checked first to see if they had any locks on resources that would concern us though the locks tables, thus not affecting users editing or posting. Although this was OK, I wasn’t happy enough with the way it worked. I could end up clearing up the data corruption if it didn’t work! I then thought I could try to use a Visual Studio Addin to log these users out, rather than rudely killing them off behind the scenes. This could be done based on the same script to decide when it should happen.

See idle times from the second SQL script on this page:http://dynamicsgpblogster.blogspot.co.uk/2009/04/retrieving-dynamics-gp-user-idle-time.html

Using Visual Studio Addin

I set up a system.Timer and fired it regularly to check the SQL script and then used a method I found to log the users out. http://mohdaoud.blogspot.co.uk/2008/12/programmatically-closing-for-dynamics_3512.html

I had found a way to do the logout and it worked like a charm. It works by simulating the user going to file>>exit. It does this by writing out a GP macro to a temp file that is subsequently ran. It meant that GP retained control in exactly the same manner as if a user had attempted to exit, thus would say “do you want to save changes?” and prevent logout, should the user be in the middle of anything risky. This soft logout is quite adequate for what I was trying to achieve, shaving a few more users out the system with low risk of causing any issues.

The problem was then that when I tried testing I found that normal users can’t access the system tables I needed (SYSPROCESSES) without granting extra permissions (VIEW SERVER STATE). I didn’t want to grant this. So it had been fine running as a SQL job under an elevated permission set, but not as end users.

Hooking into Global Procedures

Thus I started looking at how I could detect a user actively doing it in GP. I went looking at putting an event handler in the client GP application. I hit upon the global security procedure that is checked when you do most anything in GP. By using this procedure to update a last touched date I could pretty much check for user activity in the GP client though the add-in, much better as self contained functionality.

Googling that security global procedure, I then stumbled on a post by David Musgrave about how they had hooked onto that and some other procedures too when making the Rockton tool for inactivity monitoring. http://msgroups.net/microsoft.public.greatplains/auto-logout-of-dynamics-gp/590960 If only I’d seen this at the start of my hunt. I shamelessly added those extra procedures on top of the security procedure and it improved the behaviour, now virtually never get a auto logout when it shouldn’t have happened.

I prompt the user for fifty seconds with a countdown windows form providing an opportunity to cancel the auto logout. I also call out to a SQL stored procedure before initiating the logout (not shown here for simplicity). That stored procedure checks if we are within x% of the max user limit, out of hours or one of a group of users who are excluded from the functionality due to the nature of the work they do (think checklinks maintenance user etc). Then decides if the auto logout should be initiated. There is no point logging people out unless we are running out of user licences, and everyone should logout at the end of day.

Source Code Snippets

I have been running this rough version now on my copy of GP for testing. It just needs the rough edges smoothing, extra error checking and naming variables better. Also need to pull the time out settings from a SQL table where they can be maintained.

Following code shows the registration of the global procedure events we are interested in (see the dexterity programmers guide for more explanation on what they do). We then have a class that simply holds an instance of the timer inactivity class and prods it when we get activity to update the time of last activity. In that timer inactivity class we have a system timer that checks regularly to see if the last time the class was prodded for activity was over the threshold number of mins. If so it then first off the countdown form. This form allows users to cancel the auto logout.

 

Public Class UserInactivityForm
    Dim SecondsToLogout As Short = 50
    Private Sub Timer1_Tick(sender As System.Object, e As System.EventArgs) Handles Timer1.Tick
        lblCount.Text = SecondsToLogout.ToString
        SecondsToLogout = SecondsToLogout - CShort(1)
        If SecondsToLogout = 0 Then Me.DialogResult = Windows.Forms.DialogResult.OK

    End Sub

    Private Sub Button1_Click(sender As System.Object, e As System.EventArgs) Handles Button1.Click
        DialogResult = Windows.Forms.DialogResult.Cancel
    End Sub

    Private Sub UserInactivityForm_Load(sender As Object, e As System.EventArgs) Handles Me.Load
        Timer1.Interval = 1000
        Timer1.Enabled = True
        FlashWindow.Flash(Me)
    End Sub
End Class

 

Imports System.Windows.Forms

Public Class InactivityTimer

    Public WithEvents oApplicaitonExitTimer As Timers.Timer

    Private m_LastActivityTime As DateTime
    Public Property LastActivityTime() As DateTime
        Get
            Return m_LastActivityTime
        End Get
        Set(ByVal value As DateTime)
            m_LastActivityTime = value
        End Set
    End Property

    Private m_TimeOutTimeMins As Short = 1
    Public Property TimeOutTimeMins() As Short
        Get
            Return m_TimeOutTimeMins
        End Get
        Set(ByVal value As Short)
            m_TimeOutTimeMins = value
        End Set
    End Property

    Private m_Enabled As Boolean
    Public Property Enabled() As Boolean
        Get
            Return m_Enabled
        End Get
        Set(ByVal value As Boolean)
            m_Enabled = value
            If Not IsNothing(oApplicaitonExitTimer) Then
                oApplicaitonExitTimer.Enabled = value
                oApplicaitonExitTimer.Start()
            End If
        End Set
    End Property


    Public Sub Tick()
        Me.LastActivityTime = Now()
    End Sub

    Private o As UserInactivityForm


    Private Sub oApplicaitonExitTimer_Elapsed(sender As Object, e As System.Timers.ElapsedEventArgs) Handles oApplicaitonExitTimer.Elapsed
        'Checks to see if the current user has been idle too long and logs them out if so
        CheckUserTimeOut()
        'After first timeout check now check every 5mins
        'If oApplicaitonExitTimer.Interval > 300000 Then
        '    oApplicaitonExitTimer.Interval = 300000
        'End If
        'CurrentUser.CheckUserTimeOut()
    End Sub



    Public Sub TryToLogout()
        Try
            If IsNothing(o) Then o = New UserInactivityForm
            If o.ShowDialog() = Windows.Forms.DialogResult.OK Then
                'Creates a temp macro and executes it
                Dim CompilerApp As New Dynamics.Application
                Dim CompilerMessage As String = Nothing
                Dim CompilerError As Integer
                Dim Commands As String
                Commands = ""
                Commands = Commands & "local integer l_file_id; " & vbCrLf
                Commands = Commands & "local string pathname; " & vbCrLf
                Commands = Commands & "pathname = Path_GetForApp(1) + ""TEMP_LOGOUT.MAC""; " &
                vbCrLf
                Commands = Commands & "l_file_id = TextFile_Open(pathname, 0, 0); " &
                vbCrLf
                Commands = Commands & "TextFile_WriteLine(l_file_id, ""CommandExec form BuiLtin command cmdQuitApplication""); " & vbCrLf
                Commands = Commands & "TextFile_Close(l_file_id); " & vbCrLf
                Commands = Commands & "if File_Probe(pathname) then " & vbCrLf
                Commands = Commands & "  run macro pathname; " & vbCrLf
                Commands = Commands & "end if; " & vbCrLf
                ' Execute SanScript 
                CompilerError = CompilerApp.ExecuteSanscript(Commands, CompilerMessage)
                If CompilerError <> 0 Then
                    MsgBox(CompilerMessage)
                End If
            Else
                o.Dispose()
                o = Nothing
            End If
        Catch ex As Exception
            Windows.Forms.MessageBox.Show(ex.ToString)
        End Try

    End Sub

    Private Sub CheckUserTimeOut()
        'If the last activity was longer than the timeout then try logout
        If DateDiff(DateInterval.Minute, Me.LastActivityTime, Now()) > TimeOutTimeMins Then
            TryToLogout()
        End If
    End Sub


    Public Sub New()
        oApplicaitonExitTimer = New Timers.Timer(60000)
        oApplicaitonExitTimer.AutoReset = True
        Me.oApplicaitonExitTimer.Enabled = False
    End Sub

End Class
'Handlers looking for activity for the inactivity monitor
AddHandler DynamicsGP.Procedures.AddSuccessfulLoginRecord.InvokeAfterOriginal, AddressOf oGlobalProcedures.AfterSucessfulLogin
AddHandler DynamicsGP.Procedures.Pathname.InvokeAfterOriginal, AddressOf oGlobalProcedures.DynamicsProceduresPathnameInvokeAferOriginal
AddHandler DynamicsGP.Procedures.SqlScriptPath.InvokeAfterOriginal, AddressOf oGlobalProcedures.DynamicsProceduresSqlScriptPathInvokeAfterOriginal
AddHandler DynamicsGP.Procedures.SqlPath.InvokeAfterOriginal, AddressOf oGlobalProcedures.DynamicsProceduresSqlPathInvokeAfterOriginal

AddHandler DynamicsGP.Procedures.Security.InvokeAfterOriginal, AddressOf oGlobalProcedures.DynamicsProceduresSecurityInvokeAfterOriginal
AddHandler DynamicsGP.Procedures.CheckForNote.InvokeAfterOriginal, AddressOf oGlobalProcedures.DynamicsProceduresCheckForNoteInvokeAfterOriginal
AddHandler DynamicsGP.Procedures.CheckForRecordNote.InvokeAfterOriginal, AddressOf oGlobalProcedures.DynamicsProceduresCheckForRecordNoteInvokeAfterOriginal
Imports System.Threading
Imports System.Security
Imports Microsoft.Dexterity.Bridge
Imports Microsoft.Dexterity.Applications
Imports System.Windows.Forms


Public Class GlobalProcedures
    Private oInactivityTimer As New InactivityTimer

    Public Sub DynamicsProceduresPathnameInvokeAferOriginal(sender As Object, e As DynamicsDictionary.PathnameProcedure.InvokeEventArgs)
        oInactivityTimer.Tick()
    End Sub

    Public Sub DynamicsProceduresSqlScriptPathInvokeAfterOriginal(sender As Object, e As DynamicsDictionary.SqlScriptPathProcedure.InvokeEventArgs)
        oInactivityTimer.Tick()
    End Sub

    Public Sub DynamicsProceduresSqlPathInvokeAfterOriginal(sender As Object, e As DynamicsDictionary.SqlPathProcedure.InvokeEventArgs)
        oInactivityTimer.Tick()
    End Sub

    Public Sub DynamicsProceduresSecurityInvokeAfterOriginal(sender As Object, e As DynamicsDictionary.SecurityProcedure.InvokeEventArgs)
        oInactivityTimer.Tick()
    End Sub

    Public Sub DynamicsProceduresCheckForNoteInvokeAfterOriginal(sender As Object, e As DynamicsDictionary.CheckForNoteProcedure.InvokeEventArgs)
        oInactivityTimer.Tick()
    End Sub

    Public Sub DynamicsProceduresCheckForRecordNoteInvokeAfterOriginal(sender As Object, e As DynamicsDictionary.CheckForRecordNoteProcedure.InvokeEventArgs)
        oInactivityTimer.Tick()
    End Sub

    Public Sub AfterSucessfulLogin(ByVal sender As Object, ByVal e As EventArgs)
        'Only allow auto logout after login...
        Try
            oInactivityTimer.Enabled = True

        Catch ex As Exception
            Windows.Forms.MessageBox.Show("Error on after open" & vbCrLf & ex.ToString, "Toolbar AfterSucessfulLogin", MessageBoxButtons.OK, MessageBoxIcon.Information)
        End Try

    End Sub

End Class

Enhanced Version

I got around to adding a SQL check that the user count is high and excludes and key users such as sa or DYNSA. The following stored procedure checks for “permission” to log the user out. You could group users in this SQL into groups each with their own thresholds for example.

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: Tim Wappat
-- Create date: 3rd Sept 2013
-- Description: When a user session inactivity times out
-- determines if the user should be logged off
-- =============================================
CREATE PROCEDURE gpmods.SY_UserInactivityTimeOutCheck
@UserID CHAR (15)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @UserCountThreshold AS INT;
SET @UserCountThreshold = 72;
IF ((SELECT COUNT(*)
FROM dynamics..ACTIVITY) > @UserCountThreshold
AND (@USERID NOT IN ('sa', 'DYNSA')))
BEGIN
RETURN 1;
END
ELSE
BEGIN
RETURN 0;
END
END
GO
GRANT EXECUTE ON gpmods.SY_UserInactivityTimeOutCheck TO DYNGRP
GO
 
Global Procedures Class handles GP events and contains the inactivity timer
Imports System.Threading
Imports System.Security
Imports Microsoft.Dexterity.Bridge
Imports Microsoft.Dexterity.Applications
Imports System.Windows.Forms


Public Class GlobalProcedures
Private oInactivityTimer As New InactivityTimer(60000)
'60000 1 min

Public Sub DynamicsProceduresPathnameInvokeAferOriginal(sender As Object, _
e As DynamicsDictionary.PathnameProcedure.InvokeEventArgs)
Try
oInactivityTimer.Tick()
Catch ex As Exception
exceptions.ExceptionHandler.ShowException(ex)
End Try

End Sub

Public Sub DynamicsProceduresSqlScriptPathInvokeAfterOriginal(sender As Object, _
e As DynamicsDictionary.SqlScriptPathProcedure.InvokeEventArgs)
Try
oInactivityTimer.Tick()
Catch ex As Exception
exceptions.ExceptionHandler.ShowException(ex)
End Try
End Sub

Public Sub DynamicsProceduresSqlPathInvokeAfterOriginal(sender As Object, _
e As DynamicsDictionary.SqlPathProcedure.InvokeEventArgs)
Try
oInactivityTimer.Tick()
Catch ex As Exception
exceptions.ExceptionHandler.ShowException(ex)
End Try
End Sub

Public Sub DynamicsProceduresSecurityInvokeAfterOriginal(sender As Object, _
e As DynamicsDictionary.SecurityProcedure.InvokeEventArgs)
Try
oInactivityTimer.Tick()
Catch ex As Exception
exceptions.ExceptionHandler.ShowException(ex)
End Try
End Sub

Public Sub DynamicsProceduresCheckForNoteInvokeAfterOriginal(sender As Object, _
e As DynamicsDictionary.CheckForNoteProcedure.InvokeEventArgs)
Try
oInactivityTimer.Tick()
Catch ex As Exception
exceptions.ExceptionHandler.ShowException(ex)
End Try
End Sub

Public Sub DynamicsProceduresCheckForRecordNoteInvokeAfterOriginal(sender As Object, _
e As DynamicsDictionary.CheckForRecordNoteProcedure.InvokeEventArgs)
Try
oInactivityTimer.Tick()
Catch ex As Exception
exceptions.ExceptionHandler.ShowException(ex)
End Try
End Sub

Public Sub New()
oInactivityTimer.Enabled = True
End Sub
End Class
InactivityTimer class fires (60 mins in this example) to check for inactivity and logs the user out if no activity since last check and after checking with the sql stored proc that we actually need to free some users up.

Imports System.Windows.Forms

Public Class InactivityTimer

Public WithEvents oApplicaitonExitTimer As Timers.Timer

Private m_LastActivityTime As DateTime
Public Property LastActivityTime() As DateTime
Get
SyncLock (oLockLastActivityTime)
Return m_LastActivityTime
End SyncLock

End Get
Set(ByVal value As DateTime)
SyncLock (oLockLastActivityTime)
m_LastActivityTime = value
End SyncLock
End Set
End Property

Private m_TimeOutTimeMins As Short = 60
Public Property TimeOutTimeMins() As Short
Get
Return m_TimeOutTimeMins
End Get
Set(ByVal value As Short)
m_TimeOutTimeMins = value
End Set
End Property

Private m_Enabled As Boolean
Public Property Enabled() As Boolean
Get
Return m_Enabled
End Get
Set(ByVal value As Boolean)
m_Enabled = value
If Not IsNothing(oApplicaitonExitTimer) Then
oApplicaitonExitTimer.Enabled = value
oApplicaitonExitTimer.Start()
End If
End Set
End Property

Private oLockLastActivityTime As New Object
Private oLockLogout As New Object

Public Sub Tick()
Me.LastActivityTime = Now()
End Sub

Private oUserInactivityForm As UserInactivityForm


Private Sub oApplicaitonExitTimer_Elapsed(sender As Object, _
e As System.Timers.ElapsedEventArgs) _
Handles oApplicaitonExitTimer.Elapsed
'Checks to see if the current user has been idle too long and logs them out if so
CheckUserTimeOut()
End Sub


Private isLoggingOut As Boolean = False

Public Sub TryToLogout()
Try
'Test and set pattern to ensure another thread is not updating this flag
If Not isLoggingOut Then
SyncLock (oLockLogout)
If Not isLoggingOut Then
isLoggingOut = True
End If
End SyncLock
'Whatever the case, this is now a clean read
If isLoggingOut Then
If IsNothing(oUserInactivityForm) Then oUserInactivityForm = _
New UserInactivityForm
If oUserInactivityForm.ShowDialog() = Windows.Forms.DialogResult.OK Then
'Creates a temp macro and executes it
Dim CompilerApp As New Dynamics.Application
Dim CompilerMessage As String = Nothing
Dim CompilerError As Integer
Dim Commands As String
Commands = ""
Commands = Commands & "local integer l_file_id; " & vbCrLf
Commands = Commands & "local string pathname; " & vbCrLf
Commands = Commands & "pathname = Path_GetForApp(1) + ""TEMP_LOGOUT.MAC""; " &
vbCrLf
Commands = Commands & "l_file_id = TextFile_Open(pathname, 0, 0); " &
vbCrLf
Commands = Commands & "TextFile_WriteLine(l_file_id, ""CommandExec form BuiLtin command cmdQuitApplication""); " & vbCrLf
Commands = Commands & "TextFile_Close(l_file_id); " & vbCrLf
Commands = Commands & "if File_Probe(pathname) then " & vbCrLf
Commands = Commands & " run macro pathname; " & vbCrLf
Commands = Commands & "end if; " & vbCrLf
' Execute SanScript
CompilerError = CompilerApp.ExecuteSanscript(Commands, CompilerMessage)
If CompilerError <> 0 Then
MsgBox(CompilerMessage)
End If
oUserInactivityForm.Dispose()
oUserInactivityForm = Nothing
Else
Tick()
oUserInactivityForm.Dispose()
oUserInactivityForm = Nothing
SyncLock (oLockLogout)
isLoggingOut = False
End SyncLock
End If
End If
End If
Catch ex As Exception
exceptions.ExceptionHandler.ShowException(ex)
End Try

End Sub

Private Sub CheckUserTimeOut()
'If the last activity was longer than the timeout then try logout
If DateDiff(DateInterval.Minute, Me.LastActivityTime, Now()) > TimeOutTimeMins And Not isLoggingOut Then
If CheckDBForPermissionToLogout() Then
TryToLogout()
End If
End If
End Sub

Private Function CheckDBForPermissionToLogout() As Boolean
'Allows a stored to procedure to look at the activity table and decide if the user needs to be logged out,
' i.e. only log user out when activity table is nearing the user limit
' also prevents sa and some other users from getting logged out
' if connection is down or something to the database, error means we return true and the client logs out
Try

Using sql As New SqlClient.SqlConnection(CurrentUser.ConnectionStringSql)
sql.Open()
Using cmd As New SqlClient.SqlCommand("gpmods.SY_UserInactivityTimeOutCheck", sql)
cmd.CommandType = CommandType.StoredProcedure
cmd.Parameters.AddWithValue("@USERID", CurrentUser.UserID)
Dim paramReturnValue As New SqlClient.SqlParameter("@RETURN_VALUE", SqlDbType.Int)
paramReturnValue.Direction = ParameterDirection.ReturnValue
cmd.Parameters.Add(paramReturnValue)
cmd.ExecuteNonQuery()
Dim myReturnValue As Integer = CInt(paramReturnValue.Value)
If myReturnValue = 0 Then
Return False
Else
Return True
End If
End Using
End Using
Catch ex1 As SqlClient.SqlException
Return True
Catch ex As Exception
Throw
End Try
End Function

Public Sub New(TimeOutCheckFrequencySeconds As Integer)
oApplicaitonExitTimer = New Timers.Timer(TimeOutCheckFrequencySeconds)
'60000 1 min
oApplicaitonExitTimer.AutoReset = True
Me.oApplicaitonExitTimer.Enabled = False
End Sub

End Class

The add-in when initialised creates an instance of the GlobalsProcedures Class and registers the events of interest to keep the application alive when activity takes place.

Imports System.Windows.Forms
Imports DynamicsGP = Microsoft.Dexterity.Applications.Dynamics
Imports Microsoft.Dexterity.Applications
Imports Microsoft.Dexterity.Applications.MenusForVisualStudioToolsDictionary
Imports Microsoft.Dexterity.Bridge

Public Class GPAddIn
Implements IDexterityAddIn
Private HasInitalisedEventHandlers As Boolean


'GlobalProcedures essentially handles the inactivity time out functionality
Public oGlobalProcedures As New GlobalProcedures

Sub Initialize() Implements IDexterityAddIn.Initialize
Try
If Not HasInitalisedEventHandlers Then
RegisterEventHandlers()
HasInitalisedEventHandlers = True
End If
Catch ex As Exception
exceptions.ExceptionHandler.ShowException(ex)
End Try
End Sub


Public Sub RegisterEventHandlers()

'=========================================================================================================================================
'----------------------- Hooks used for inactivity monitoring -----------------------------
'=========================================================================================================================================
AddHandler DynamicsGP.Procedures.Pathname.InvokeAfterOriginal, AddressOf oGlobalProcedures.DynamicsProceduresPathnameInvokeAferOriginal
AddHandler DynamicsGP.Procedures.SqlScriptPath.InvokeAfterOriginal, AddressOf oGlobalProcedures.DynamicsProceduresSqlScriptPathInvokeAfterOriginal
AddHandler DynamicsGP.Procedures.SqlPath.InvokeAfterOriginal, AddressOf oGlobalProcedures.DynamicsProceduresSqlPathInvokeAfterOriginal

AddHandler DynamicsGP.Procedures.Security.InvokeAfterOriginal, AddressOf oGlobalProcedures.DynamicsProceduresSecurityInvokeAfterOriginal
AddHandler DynamicsGP.Procedures.CheckForNote.InvokeAfterOriginal, AddressOf oGlobalProcedures.DynamicsProceduresCheckForNoteInvokeAfterOriginal
AddHandler DynamicsGP.Procedures.CheckForRecordNote.InvokeAfterOriginal, AddressOf oGlobalProcedures.DynamicsProceduresCheckForRecordNoteInvokeAfterOriginal
End Sub
End Class

 

Note

 
18th May 2014: We had some problems in production with instability in GP with the first version of this code. When the timer fires it creates a new thread, so a little attention to detail is required to handle multi-threaded variable access, hence in the more recent version it uses sync locks. Now those problems should be eradicated.
 

Credits

Thanks to Mohammad R. Daou and David Musgrave for their contributions to this project via forums and blogs.

Comments (15) -

  • Hi Tim did you ever find the solution for the thread locking?
  • Tim
    Thanks for the reminder. I'll check it out over the Christmas break and write it up.
  • Any update on the code? We are just running into the same issue.
  • OK - looks like there is a bit of interest in this.
    I'll break this project back out and have a look at the issue and write it up.
  • Guy
    Just to add a bit more interest please Tim. This is a solution that could work very well for us too on a different database for the same reasons if you could help. Thanks.
  • I have updated the post to reflect the changes I made to prevent the threading issues.
  • Tim
    Arthur, I don't think thread safe is quite the right term. The System.Windows.Forms.Timer is useful in windows forms applications as the events are raised on the UI thread, the other timers use the tread pool to create a new thread on which the event is raised. Hence you have to be thread aware from raising the event onward.
    Sync Locking the resources hopefully solves the issues. Although I've been running this code personally, due us not hitting the limit threshold much at the moment, I've not tested it in anger with the users yet.
    I'd be very interested if it works for you.
  • Hi Tim, only a couple of days in but seems stable, had no complaints yet.

    The only way I can make it unstable in my testing is by pushing the limits on how often the timer checks...GP doesn't like me reading the userid from the globals that often it seems. Anything under 30 seconds and it will go nuts. If I remove the reference to the userid then you can really push the checking and it remains stable.

    Cheers
  • Hi Tim,

    Just a note, for GP2013 R2 + you can use the following function to get the number of users logged on if you don't want to use the SQL enhanced functionality you mention in your article.

                '------------------------------------------------------------------------
                'SYSTEM GLOBAL PROCEDURE:  smGetLoggedInUserCount
                '------------------------------------------------------------------------
                'sproc returns            long                     smGetLoggedInUserCount;
                'in                       integer                  nUserType;
                'inout                    long                     nUserCount;



                DynamicsGP.Procedures.SmGetLoggedInUserCount.Invoke(0, 1, NumberofUsers) 'True, FullUser=1, Numberofusers=total users logged on.

    • Really cool to get your comment this morning. I like this, makes things much more core!
      • Thanks Tim, just trying to make it even better if possible...now to find a function to replace the logging off as I hate using continuum haha.
        • I see our user count creeping up again at the moment, so might be time for me to revisit this ourselves too, so be interested in how you get on.
          • Hi Tim,

            I have been using it since your original version.

            The only issue I ever find is that people might get stuck with processes running and I suspect the constant checking might be part of the cause as I can get that to occur more often if I check every 30 seconds instead of every minute.

            It does seem to be less of an issue atm since we got a new sql server but still happens a couple of times a day when we have over 95% users logged on.

            I have made a couple of changes to the code in response to reading part two and deployed them over the weekend so I will keep an eye on it.

            Cheer

  • Your blog is very useful and provides tremendous facts. Keep up the good work.

Pingbacks and trackbacks (1)+

Add comment

Loading