Epicor 10 MES Customization: Auto Logout

If you use MES on the shop floor, chances are people occasionally walk away from the terminal while they are still logged in. We were doing a big Epicor 10 MES customization project for a customer, and the question came up whether we could have users automatically log out if they were not using the terminal for a while. It took a little Windows API in the customization, but we figured it out - now here it is for your enjoyment.

Let’s start with a barebones customization that implements a basic timer logic and break down the details:

// **************************************************
// Custom code for MESMenu
// Created: 5/23/2019 3:24:17 PM
// **************************************************
using System;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Windows.Forms;
using Ice.Lib.Customization;
using Ice.Lib.ExtendedProps;
using Ice.Lib.Framework;
using Ice.Lib.Searches;
using Ice.UI.FormFunctions;

// Added for timer support
using System.Runtime.InteropServices;

public class Script
{
	// ** Wizard Insert Location - Do Not Remove 'Begin/End Wizard Added Module Level Variables' Comments! **
	// Begin Wizard Added Module Level Variables **

	// End Wizard Added Module Level Variables **

	// Add Custom Module Level Variables Here **
	SystemIdleTimer idleTimer = new SystemIdleTimer();

	public void InitializeCustomCode()
	{
		// ** Wizard Insert Location - Do not delete 'Begin/End Wizard Added Variable Initialization' lines **
		// Begin Wizard Added Variable Initialization

		// End Wizard Added Variable Initialization

		// Begin Wizard Added Custom Method Calls

		// End Wizard Added Custom Method Calls
	}

	public void DestroyCustomCode()
	{
		// ** Wizard Insert Location - Do not delete 'Begin/End Wizard Added Object Disposal' lines **
		// Begin Wizard Added Object Disposal

		// End Wizard Added Object Disposal

		// Begin Custom Code Disposal
		idleTimer.OnEnterIdleState -= new SystemIdleTimer.OnEnterIdleStateEventHandler(idleTimer_OnEnterIdleState);
		idleTimer.OnExitIdleState -= new SystemIdleTimer.OnExitIdleStateEventHandler(idleTimer_OnExitIdleState);
		idleTimer.Dispose();
		// End Custom Code Disposal
	}

	private void idleTimer_OnEnterIdleState(object sender, IdleEventArgs args)
	{
		// This is where you put the code to deal with when the session has been detected
		// as entering an idle state
		MessageBox.Show("Get back to work Adam!");
		idleTimer.Start();
	}

	private void idleTimer_OnExitIdleState(object sender, IdleEventArgs args)
	{
		// This is where you put the code for when the session was idle and now has
		// been re-activated.  You might not need this for anything, but thought
		// it would be good to point out.
		MessageBox.Show("Good boy");
		idleTimer.Start();
	}

	private void MESMenu_Load(object sender, EventArgs args)
	{
		idleTimer.OnEnterIdleState += new SystemIdleTimer.OnEnterIdleStateEventHandler(idleTimer_OnEnterIdleState);
		idleTimer.OnExitIdleState += new SystemIdleTimer.OnExitIdleStateEventHandler(idleTimer_OnExitIdleState);
		idleTimer.MaxIdleTime = 10; // Seconds
		idleTimer.Start();
	}
}


// Added for timer support
public class IdleEventArgs : EventArgs {
        
        private DateTime m_EventTime;
        
        public DateTime EventTime {
            get {
                return m_EventTime;
            }
        }
        
        public IdleEventArgs(DateTime timeOfEvent) {
            m_EventTime = timeOfEvent;
        }
    }

public class SystemIdleTimer : Component
{
	private const double INTERNAL_TIMER_INTERVAL = 550;
	[Description("Event that if fired when idle state is entered.")]
	public event OnEnterIdleStateEventHandler OnEnterIdleState;
	public delegate void OnEnterIdleStateEventHandler(object sender, IdleEventArgs e);
	[Description("Event that is fired when leaving idle state.")]
	public event OnExitIdleStateEventHandler OnExitIdleState;
	public delegate void OnExitIdleStateEventHandler(object sender, IdleEventArgs e);

	private System.Timers.Timer ticker;
	private int m_MaxIdleTime;
	private object m_LockObject;

	private bool m_IsIdle = false;

	[Description("Maximum idle time in seconds.")]
	public int MaxIdleTime {
		get { return m_MaxIdleTime; }
		set {
			if (value == 0) {
				throw new ArgumentException("MaxIdleTime must be larger then 0.");
			} else {
				m_MaxIdleTime = value;
			}
		}
	}
	public SystemIdleTimer()
	{
		m_LockObject = new object();
		ticker = new System.Timers.Timer(INTERNAL_TIMER_INTERVAL);
		ticker.Elapsed += InternalTickerElapsed;
	}
	public void Start()
	
		
	
	public void Stop()
	{
		ticker.Stop();
		lock (m_LockObject) {
			m_IsIdle = false;
		}
	}
	public bool IsRunning {
		get { return ticker.Enabled; }
	}
	private void InternalTickerElapsed(object sender, System.Timers.ElapsedEventArgs e)
	{
		uint idleTime = Win32Wrapper.GetIdle();
		if (idleTime > (MaxIdleTime * 1000)) {
			if (m_IsIdle == false) {
				lock (m_LockObject) {
					m_IsIdle = true;
				}
				IdleEventArgs args = new IdleEventArgs(e.SignalTime);
				if (OnEnterIdleState != null) {
					OnEnterIdleState(this, args);
				}
			}
		} else {
			if (m_IsIdle) {
				lock (m_LockObject) {
					m_IsIdle = false;
				}
				IdleEventArgs args = new IdleEventArgs(e.SignalTime);
				if (OnExitIdleState != null) {
					OnExitIdleState(this, args);
				}
			}
		}
	}
}

public class Win32Wrapper
{
	public struct LASTINPUTINFO
	{
		public uint cbSize;
		public uint dwTime;
	}

	[DllImport("User32.dll")]
	private static extern bool GetLastInputInfo(ref LASTINPUTINFO lii);

	public static uint GetIdle()
	{
		LASTINPUTINFO lii = new LASTINPUTINFO();
		lii.cbSize = Convert.ToUInt32((Marshal.SizeOf(lii)));
		GetLastInputInfo(ref lii);
		return Convert.ToUInt32(Environment.TickCount) - lii.dwTime;
	}
}

So there is a lot here, but most of it is boilerplate stuff. Let’s break it down. First, we have this using statement - it just lets us get to the juicy Windows API stuff:

using System.Runtime.InteropServices

Next let’s skip to the bottom of the customization where we add 2 new classes. Guys, we are going to be straight up with you here - we found this on some StackOverflow or something like that. So this was copy / paste job for us and it should be for your too:

public class IdleEventArgs : EventArgs {
        
        private DateTime m_EventTime;
        
        public DateTime EventTime {
            get {
                return m_EventTime;
            }
        }
        
        public IdleEventArgs(DateTime timeOfEvent) {
            m_EventTime = timeOfEvent;
        }
    }

public class SystemIdleTimer : Component
{
	private const double INTERNAL_TIMER_INTERVAL = 550;
	[Description("Event that if fired when idle state is entered.")]
	public event OnEnterIdleStateEventHandler OnEnterIdleState;
	public delegate void OnEnterIdleStateEventHandler(object sender, IdleEventArgs e);
	[Description("Event that is fired when leaving idle state.")]
	public event OnExitIdleStateEventHandler OnExitIdleState;
	public delegate void OnExitIdleStateEventHandler(object sender, IdleEventArgs e);

	private System.Timers.Timer ticker;
	private int m_MaxIdleTime;
	private object m_LockObject;

	private bool m_IsIdle = false;

	[Description("Maximum idle time in seconds.")]
	public int MaxIdleTime {
		get { return m_MaxIdleTime; }
		set {
			if (value == 0) {
				throw new ArgumentException("MaxIdleTime must be larger then 0.");
			} else {
				m_MaxIdleTime = value;
			}
		}
	}
	public SystemIdleTimer()
	{
		m_LockObject = new object();
		ticker = new System.Timers.Timer(INTERNAL_TIMER_INTERVAL);
		ticker.Elapsed += InternalTickerElapsed;
	}
	public void Start()
	
		
	
	public void Stop()
	{
		ticker.Stop();
		lock (m_LockObject) {
			m_IsIdle = false;
		}
	}
	public bool IsRunning {
		get { return ticker.Enabled; }
	}
	private void InternalTickerElapsed(object sender, System.Timers.ElapsedEventArgs e)
	{
		uint idleTime = Win32Wrapper.GetIdle();
		if (idleTime > (MaxIdleTime * 1000)) {
			if (m_IsIdle == false) {
				lock (m_LockObject) {
					m_IsIdle = true;
				}
				IdleEventArgs args = new IdleEventArgs(e.SignalTime);
				if (OnEnterIdleState != null) {
					OnEnterIdleState(this, args);
				}
			}
		} else {
			if (m_IsIdle) {
				lock (m_LockObject) {
					m_IsIdle = false;
				}
				IdleEventArgs args = new IdleEventArgs(e.SignalTime);
				if (OnExitIdleState != null) {
					OnExitIdleState(this, args);
				}
			}
		}
	}
}

public class Win32Wrapper
{
	public struct LASTINPUTINFO
	{
		public uint cbSize;
		public uint dwTime;
	}

	[DllImport("User32.dll")]
	private static extern bool GetLastInputInfo(ref LASTINPUTINFO lii);

	public static uint GetIdle()
	{
		LASTINPUTINFO lii = new LASTINPUTINFO();
		lii.cbSize = Convert.ToUInt32((Marshal.SizeOf(lii)));
		GetLastInputInfo(ref lii);
		return Convert.ToUInt32(Environment.TickCount) - lii.dwTime;
	}
}

Alright now let’s connect the pieces of the rest of the implementation. First, we have to declare an instance of our fancy new timer object:

SystemIdleTimer idleTimer = new SystemIdleTimer();

Then we add events to deal with when a user goes idle and when they come back:

	private void idleTimer_OnEnterIdleState(object sender, IdleEventArgs args)
	{
		// This is where you put the code to deal with when the session has been detected
		// as entering an idle state
		MessageBox.Show("Get back to work Adam!");
		idleTimer.Start();
	}

	private void idleTimer_OnExitIdleState(object sender, IdleEventArgs args)
	{
		// This is where you put the code for when the session was idle and now has
		// been re-activated.  You might not need this for anything, but thought
		// it would be good to point out.
		MessageBox.Show("Good boy");
		idleTimer.Start();
	}

Now on form load we wire up these events and define how many seconds of inactivity constitute being “idle”:

	private void MESMenu_Load(object sender, EventArgs args)
	{
		idleTimer.OnEnterIdleState += new SystemIdleTimer.OnEnterIdleStateEventHandler(idleTimer_OnEnterIdleState);
		idleTimer.OnExitIdleState += new SystemIdleTimer.OnExitIdleStateEventHandler(idleTimer_OnExitIdleState);
		idleTimer.MaxIdleTime = 10; // Seconds
		idleTimer.Start();
	}

Lastly, clean up your objects on dispose:

	public void DestroyCustomCode()
	{
		// ** Wizard Insert Location - Do not delete 'Begin/End Wizard Added Object Disposal' lines **
		// Begin Wizard Added Object Disposal

		// End Wizard Added Object Disposal

		// Begin Custom Code Disposal
		idleTimer.OnEnterIdleState -= new SystemIdleTimer.OnEnterIdleStateEventHandler(idleTimer_OnEnterIdleState);
		idleTimer.OnExitIdleState -= new SystemIdleTimer.OnExitIdleStateEventHandler(idleTimer_OnExitIdleState);
		idleTimer.Dispose();
		// End Custom Code Disposal
	}

Ok, so at this point you should now be able to save this customization to MES, run it, and see it nag you “Get Back To Work Adam” after 10 seconds if you don’t move the mouse and then say “Good Boy” as soon as you do move the mouse:

 
Screenshot of Epicor 10 MES customization screen where we auto-logout a user after inactivity - GingerHelp
 

Cool, so now we have successfully made something annoying! From here all you’d need to do is fire more helpful logic (at a bit more generous of a timeout interval). Here we updated the OnEnterIdleState logic to call a LogOut which just grabs a reference to the button and clicks it:

	private void idleTimer_OnEnterIdleState(object sender, IdleEventArgs args)
	{
		// This is where you put the code to deal with when the session has been detected
		// as entering an idle state
		this.LogOut();
		idleTimer.Start();
	}

	private void LogOut()
	{
		EpiButton btnLogOut = (EpiButton)csm.GetNativeControlReference("a2f6e795-4ab3-4121-bce4-e1d5f0881b0a");
		btnLogOut.PerformClick();
	} // LogOut()

So at this point, you should have a customization that logs you out automatically if you don’t have activity for 10 seconds. You will want to tweak that timing for sure, but hopefully, this gave you some ideas!

Did this blog help you out at all? If so, please leave me a comment. Same if you have any challenges, I’m here to help! Lastly, if you are looking at this and realizing you want something like it but prefer to have me do it for you - please reach out!