So this is what I need to do
There is a program which connects to some server and fetches data from it at a speed of approximately 15-20 entries per second
Right at the moment when connection with the server has been established, an Excel window opens and all entries fetched from the server, are dynamically moved into that Excel table
However all that content remains in the RAM, i.e. Excel file itself is created only when we try to close that Excel window and 'Save As..' dialog appears
As you have probably understood, I don't want to save the file manually every time, and hence I need some way to programmatically save Excel table in an opened window, into a file
Is there any way of doing that ?
If you're not running this on a server and you don't mind manually triggering the Excel save, then the you can create something that will connect to the Excel instance using Interop. The following will connect to the Excel instance and return an Excel.Workbook object for the specified workbook name:
private Excel.Workbook GetWorkbook(string workbookName)
{
Excel.Window window = null; // Excel window object from which application is grabbed
Excel.Application app = null; // Excel instance from which we get all the open workbooks
Excel.Workbooks wbs = null; // List of workbooks
Excel.Workbook wb = null; // Workbook to return
EnumChildCallback cb; // Callback routine for child window enumeration routine
List<Process> procs = new List<Process>(); // List of processes
// Get a full list of all processes that have a name of "excel"
procs.AddRange(Process.GetProcessesByName("excel"));
foreach (Process proc in procs)
{
// Make sure we have a valid handle for the window
if ((int)proc.MainWindowHandle > 0)
{
// Get the handle of the child window in the current Excel process
int childWindow = 0;
cb = new EnumChildCallback(EnumChildProc);
EnumChildWindows((int)proc.MainWindowHandle, cb, ref childWindow);
// Make sure we got a valid handle
if (childWindow > 0)
{
// Get the address of the child window so that we can talk to it and
// get all the workbooks
const uint OBJID_NATIVEOM = 0xFFFFFFF0;
Guid IID_IDispatch =
new Guid("{00020400-0000-0000-C000-000000000046}");
int res = AccessibleObjectFromWindow(childWindow, OBJID_NATIVEOM,
IID_IDispatch.ToByteArray(), ref window);
if (res >= 0)
{
app = window.Application;
wbs = app.Workbooks;
// Loop through all the workbooks within the current Excel window
// to see if any match
for (int i = 1; i <= wbs.Count; i++)
{
wb = wbs[i];
if (wb.Name == workbookName)
{
break;
}
wb = null;
}
}
}
}
// If we've already found our workbook then there's no point in continuing
// through the remaining processes
if (wb != null)
{
break;
}
}
Release(wbs);
Release(app);
Release(window);
return wb;
}
The Release() methods called above simply set the references to null and call Marshal.FinalReleaseComObject() on them, otherwise you end up with headless instances of Excel all over the place.
You'll also need the following to perform some of the functionality to grab windows:
private delegate bool EnumChildCallback(int hwnd, ref int lParam);
[DllImport("User32.dll")]
private static extern bool EnumChildWindows(int hWndParent, EnumChildCallback lpEnumFunc, ref int lParam);
[DllImport("Oleacc.dll")]
private static extern int AccessibleObjectFromWindow(int hwnd, uint dwObjectID, byte[] riid, ref Excel.Window ptr);
private bool EnumChildProc(int hwndChild, ref int lParam)
{
// Get the name of the class that owns the passed-in window handle
StringBuilder buf = new StringBuilder(128);
GetClassName(hwndChild, buf, 128);
// If the class name is EXCEL7 then we've got an valid Excel window
if (buf.ToString() == "EXCEL7")
{
lParam = hwndChild;
return false;
}
return true;
}
[DllImport("User32.dll")]
private static extern int GetClassName(int hWnd, StringBuilder lpClassName, int nMaxCount);
Once you have the workbook then you can call Workbook.SaveAs() to save it.
Related
I am writing an application in C# that changes data in an excel worksheet. VBA isn't an option because the version of office that is installed on the clients desktop is Microsoft Office 2010 Starter Edition which doesn't support VBA (none of the starter editions do). The application is using the excel interop library.
When I start the application it checks to see if the excel workbook that is to be modified is open and if it is open it notifies the user and then quits. This part is working as expected. The check isn't working if the user opens the excel file for some reason after starting the application and then trying to save their work from inside the application. In that case any modifications from the application are lost without any error notification. If you need to see more of the code to answer the entire project is in GitHub.
I've tried changing CheckExcelWorkBookOpen from a static class to a class that gets instantiated every time it is used, just in case the list of open workbooks was being stored in the excel interop library, this did not help.
The code that works in the application start up is:
CheckExcelWorkBookOpen testOpen = new CheckExcelWorkBookOpen();
testOpen.TestAndThrowIfOpen(Preferences.ExcelWorkBookFullFileSpec);
The code is also called any time the application attempts to open the file either for input or output, this doesn't work:
private void StartExcelOpenWorkbook()
{
if (xlApp != null)
{
return;
}
CheckExcelWorkBookOpen testOpen = new CheckExcelWorkBookOpen();
testOpen.TestAndThrowIfOpen(WorkbookName);
xlApp = new Excel.Application();
xlApp.Visible = false;
xlApp.DisplayAlerts = false;
xlWorkbook = xlApp.Workbooks.Open(WorkbookName);
}
Current Code
using System;
using Excel = Microsoft.Office.Interop.Excel;
namespace TenantRosterAutomation
{
public class CheckExcelWorkBookOpen
{
// Check if there is any instance of excel open using the workbook.
public static bool IsOpen(string workBook)
{
Excel.Application TestOnly = null;
bool isOpened = true;
// There are 2 possible exceptions here, GetActiveObject will throw
// an exception if no instance of excel is running, and
// workbooks.get_Item throws an exception if the sheetname isn't found.
// Both of these exceptions indicate that the workbook isn't open.
try
{
TestOnly = (Excel.Application)System.Runtime.InteropServices.Marshal.GetActiveObject("Excel.Application");
int lastSlash = workBook.LastIndexOf('\\');
string fileNameOnly = workBook.Substring(lastSlash + 1);
TestOnly.Workbooks.get_Item(fileNameOnly);
TestOnly = null;
}
catch (Exception)
{
isOpened = false;
if (TestOnly != null)
{
TestOnly = null;
}
}
return isOpened;
}
// Common error message to use when the excel file is op in another app.
public string ReportOpen()
{
string alreadyOpen = "The excel workbook " +
Globals.Preferences.ExcelWorkBookFullFileSpec +
" is alread open in another application. \n" +
"Please save your changes in the other application and close the " +
"workbook and then try this operation again or restart this application.";
return alreadyOpen;
}
public void TestAndThrowIfOpen(string workBook)
{
if (IsOpen(workBook))
{
AlreadyOpenInExcelException alreadOpen =
new AlreadyOpenInExcelException(ReportOpen());
throw alreadOpen;
}
}
}
}
This code is now included in a question on code review.
I got the above code to work by ensuring that any excel process started by the application was killed after the task was complete. The following code is added to my ExcelInterface module. The Dispose(bool) function already existed but did not kill the process:
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
if (xlWorkbook != null)
{
xlWorkbook.Close();
xlWorkbook = null;
}
if (xlApp != null)
{
xlApp.Quit();
xlApp = null;
Process xlProcess = Process.GetProcessById(ExcelProcessId);
if (xlProcess != null)
{
xlProcess.Kill();
}
}
}
disposed = true;
}
}
[DllImport("user32.dll")]
static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
private int GetExcelProcessID(Excel.Application excelApp)
{
int processId;
GetWindowThreadProcessId(excelApp.Hwnd, out processId);
return processId;
}
private void StartExcelOpenWorkbook()
{
if (xlApp != null)
{
return;
}
CheckExcelWorkBookOpen testOpen = new CheckExcelWorkBookOpen();
testOpen.TestAndThrowIfOpen(WorkbookName);
xlApp = new Excel.Application();
xlApp.Visible = false;
xlApp.DisplayAlerts = false;
xlWorkbook = xlApp.Workbooks.Open(WorkbookName);
ExcelProcessId = GetExcelProcessID(xlApp);
}
I am facing issue while automating a file upload process through chrome browser.
I have handled the Choose file windows popup. I use the following batch file to log off from remote desktop connnection .
for /f "skip=1 tokens=3" %%s in ('query user %USERNAME%') do (
%windir%\System32\tscon.exe %%s /dest:console
)
I have already disabled every setting under Administrative Templates > Windows Components > Remote Desktop Session Host > Session Time Limits in Windows Group Policy.
I have used below code for handling the windows file upload dialog
var title = "Open";
var fileTextBoxName = "File name:";
var openButtonName = "File name:";
//File Upload Window
AutomationElement desktopObject = AutomationElement.RootElement;
PropertyCondition popUpWindowNameCondition = new PropertyCondition(AutomationElement.NameProperty, title.Trim());
AutomationElement popUpWindow = desktopObject.FindFirst(TreeScope.Subtree, popUpWindowNameCondition);
if (popUpWindow == null) throw new Exception("File Upload dialog window not found");
// FileTextBox
PropertyCondition fileTextBoxCondition = new PropertyCondition(AutomationElement.NameProperty, fileTextBoxName.Trim());
AutomationElement fileTextBox = popUpWindow.FindFirst(TreeScope.Subtree,
new AndCondition(fileTextBoxCondition,
new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Edit)));
if (fileTextBox == null) throw new Exception("File textbox not found");
try
{
SetForegroundWindow(popUpWindow, 0);
}
catch (Exception ex)
{
}
Thread.Sleep(2000);
try
{
fileTextBox.SetFocus();
}
catch (Exception ex)
{
}
ValuePattern etb = fileTextBox.GetCurrentPattern(ValuePattern.Pattern) as ValuePattern;
etb.SetValue(extractedFields["UploadFilePath"].ToString());
Thread.Sleep(2000);
PropertyCondition buttonCondition = new PropertyCondition(AutomationElement.NameProperty, "Open");
AutomationElement buttonElement = popUpWindow.FindFirst(TreeScope.Descendants, buttonCondition);
if (buttonElement == null) throw new Exception("Button not found");
try
{
AutomationElement button = buttonElement;
InvokePattern buttonPattern = button.GetCurrentPattern(InvokePattern.Pattern) as InvokePattern;
buttonPattern.Invoke();
}
catch (Exception ex)
{
SendKeys.SendWait("{Enter}");
}
static int RETRY_LIMIT = 3;
public static bool SetForegroundWindow(AutomationElement elm, uint retries, int time = 2000)
{
try
{
if (retries < RETRY_LIMIT)
{
// Using Win32 to set foreground window because
// AutomationElement.SetFocus() is unreliable
// Get handle to the element
IntPtr other = FindWindow(null, elm.Current.Name);
// Get the Process ID for the element we are trying to
// set as the foreground element
int other_id = GetWindowThreadProcessId(
other, IntPtr.Zero);
// Get the Process ID for the current process
int this_id = GetWindowThreadProcessId(
Process.GetCurrentProcess().Handle, IntPtr.Zero);
// Attach the current process's input to that of the
// given element. We have to do this otherwise the
// WM_SETFOCUS message will be ignored by the element.
bool success =
AttachThreadInput(this_id, other_id, true);
// Make the Win32 call
IntPtr previous = SetForegroundWindow(other);
if (IntPtr.Zero.Equals(previous))
{
// Trigger re-try
throw new Exception(
"SetForegroundWindow failed");
}
return true;
}
// Exceeded retry limit, failed!
return false;
}
catch
{
retries++;
Thread.Sleep(time);
return SetForegroundWindow(elm, retries);
}
}
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern bool AttachThreadInput(int idAttach, int idAttachTo, bool fAttach);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern int GetWindowThreadProcessId(IntPtr hWnd, IntPtr lpdwProcessId);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern IntPtr SetForegroundWindow(IntPtr hWnd);
The code works perfectly after disconnecting from RDP using above bat. But the issue starts afters approx 12 hours. There is no error logged in the log file. (I have removed the logs from below code for keeping it small). The code finds the open dialog box and even the file textbox. The log says it has entered the file path. And even the open button is clicked . But actually the file is not selected . The website throws a alert prompting for selecting the file on pressing the upload button. The issue is only resolved when i reconnect to RDP and use the above bat to disconnect from RDP. I want to know is there any possibility for the code to work without reconnecting to RDP after every 12 hours ?
I am trying to close excel process in my winform application. I have gone through lots of posts on SO and other sites and this is what I am doing right now:
private void lsvSelectedQ_SelectedIndexChanged(object sender, EventArgs e)
{
FillSelectedItems();
}
private void FillSelectedItems()
{
string filepath = string.Empty;
string reportname = lblreportname.Text;
filepath = Application.StartupPath + "\\StandardReports\\" + reportname + ".xls";
Microsoft.Office.Interop.Excel.Application xlApp;
Microsoft.Office.Interop.Excel.Workbook xlWorkBook;
Microsoft.Office.Interop.Excel.Worksheet xlWorkSheet;
object misValue = System.Reflection.Missing.Value;
xlApp = new Microsoft.Office.Interop.Excel.Application();
xlWorkBook = xlApp.Workbooks.Open(filepath, 0, true, 5, "", "", true, Microsoft.Office.Interop.Excel.XlPlatform.xlWindows, "\t", false, false, 0, true, 1, 0);
List<string> WorksheetList = new List<string>();
xlWorkSheet = (Microsoft.Office.Interop.Excel.Worksheet)xlWorkBook.Worksheets.get_Item("Config");
Microsoft.Office.Interop.Excel.Range objRange = null;
objRange = (Microsoft.Office.Interop.Excel.Range)xlWorkSheet.Cells[6, 2];
if (objRange.Value != null)
{
int intSheet = Convert.ToInt32(objRange.Value);
for (int i = 0; i < intSheet; i++)
{
objRange = (Microsoft.Office.Interop.Excel.Range)xlWorkSheet.Cells[6, 3+i ];
if (objRange.Value != null)
{
lsvSelectedQ.Items.Add(Convert.ToString(objRange.Value));
}
}
}
ReleaseMyExcelsObjects(xlApp, xlWorkBook, xlWorkSheet);
}
In the above code I am using ReleaseMyExcelsObjects method to get rid of running excel process in the taskbar.
private void ReleaseMyExcelsObjects(Microsoft.Office.Interop.Excel.Application xlApp, Microsoft.Office.Interop.Excel.Workbook xlWorkBook, Microsoft.Office.Interop.Excel.Worksheet xlWorkSheet)
{
xlApp.DisplayAlerts = false;
xlWorkBook.Save();
xlWorkBook.Close();
xlApp.DisplayAlerts = false;
xlApp.Quit();
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlWorkSheet);
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlWorkBook);
System.Runtime.InteropServices.Marshal.ReleaseComObject(xlApp);
xlWorkSheet = null;
xlWorkBook = null;
xlApp = null;
GC.Collect();
}
As you can see I open an excel on a SelectedIndexChanged and I am also trying to close the process through ReleaseMyExcelsObjects() method and it works except for the first excel process that is generated. I mean when the event is fired the excel process is started and ReleaseMyExcelsObjects() does not close it however the second time SelectedIndexChanged is fired, another excel process is started. This time ReleaseMyExcelsObjects() closes this second excel process. But the first excel process which was started when theSelectedIndexChanged event was fired for the first time never gets closed.
EDIT:
I have posted an answer myself which is getting the job done. But I am going to keep this question open if in case someone comes up with better solution.
I have found a work around for this:
[System.Runtime.InteropServices.DllImport("user32.dll")]
static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
public static Process GetExcelProcess(Microsoft.Office.Interop.Excel.Application excelApp)
{
int id;
GetWindowThreadProcessId(excelApp.Hwnd, out id);
return Process.GetProcessById(id);
}
Use it as classobj.GetExcelProcess(xlApp).Kill();
Taken from : https://stackoverflow.com/a/15556843/2064292
from my understanding, the ReleaseComObject closes the Excel Application when your C# application exits. Thus, a new Excel processes will show up in your taskbar every time you call FillSelectedItems() which won't close until you exit your C# application.
EDIT: On a side note, I recommend using try, catch, finally when handling the excel interop library, mainly due to the fact that if the application runs into an exception, the program will not exit normally, and as stated before it will result on the excel process remaining in the taskbar (and when you manually open said file on excel it will tell you the last recovered version blablabla)
string filepath = string.Empty;
string reportname = lblreportname.Text;
filepath = Application.StartupPath + "\\StandardReports\\" + reportname + ".xls";
Microsoft.Office.Interop.Excel.Application xlApp;
Microsoft.Office.Interop.Excel.Workbook xlWorkBook;
Microsoft.Office.Interop.Excel.Worksheet xlWorkSheet;
object misValue = System.Reflection.Missing.Value;
xlApp = new Microsoft.Office.Interop.Excel.Application();
xlWorkBook = xlApp.Workbooks.Open(filepath, 0, true, 5, "", "", true, Microsoft.Office.Interop.Excel.XlPlatform.xlWindows, "\t", false, false, 0, true, 1, 0);
List<string> WorksheetList = new List<string>();
try
{
//Your code
}
catch (Exception ex) { MessageBox.Show(string.Format("Error: {0}", ex.Message)); }
finally
{
xlWorkBook.Close(false, filepath, null);
Marshal.ReleaseComObject(xlWorkBook);
}
Honestly, if you are annoyed by this, I would recommend using EPPlus or ExcelDataReader(this one only for reading the excel spreadsheets) as alternative libraries. Otherwise, No matter how many releaseComObjects or garbagecollectors you add, I believe you wont get rid of this issue.
EDIT 2: Of course, another way to go around this is to search for an Excel process ID and kill it. The reason I did not recommend this, is because in the event that the user who's running this application already has another excel process running on his computer, you could end up killing that instance.
I have a solution for closing the Excel process. Instead of going thru the pains of releasing your objects, you can kill that specific excel process (if you have multiple of them open).
The code is:
using System;
using System.Runtime.InteropServices;
using Microsoft.Office.Interop.Excel;
namespace YourNameSpace
{
public class MicrosoftApplications
{
[DllImport("user32.dll")]
static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
public class Excel
{
public Excel()
{
Application = new Microsoft.Office.Interop.Excel.Application();
RegisterExitEvent();
}
public Microsoft.Office.Interop.Excel.Application Application;
private void RegisterExitEvent()
{
Application.WindowDeactivate -= XlApp_WindowDeactivate;
Application.WindowDeactivate += XlApp_WindowDeactivate;
}
private void XlApp_WindowDeactivate(Workbook Wb, Window Wn)
{
Kill();
}
public void Kill()
{
int pid = 0;
GetWindowThreadProcessId(Application.Hwnd, out pid);
if (pid > 0)
{
System.Diagnostics.Process p = System.Diagnostics.Process.GetProcessById(pid);
p.Kill();
}
Application = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
}
}
And you can call it by: YourNameSpace.MicrosoftApplications.Excel xlApp = new YourNameSpace.MicrosoftApplications.Excel();
Do whatever you need to do by calling xlApp.Application.whatever instead of xlApp.whatever and if the user exits the excel window(s) it will kill the process(es) that were used in the code. If you want to just generate a report behind the scenes but not display the form, then simply call xlApp.Kill(); to end that specific process.
I'm attempting to launch a service using CreateProcessAsUser but for some reason multiple (30+) instances of the EXE are being created when debugging. The processes begin to spawn on this line of code:
ret = CreateProcessAsUser(DupedToken, Path, null, ref sa, ref sa, false, 0, (IntPtr)0, "c:\\", ref si, out pi);
I used code from this example - http://support.microsoft.com/default.aspx?scid=kb;EN-US;889251.
[StructLayout(LayoutKind.Sequential)]
public struct STARTUPINFO
{
public int cb;
public String lpReserved;
public String lpDesktop;
public String lpTitle;
public uint dwX;
public uint dwY;
public uint dwXSize;
public uint dwYSize;
public uint dwXCountChars;
public uint dwYCountChars;
public uint dwFillAttribute;
public uint dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public uint dwProcessId;
public uint dwThreadId;
}
[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
public int Length;
public IntPtr lpSecurityDescriptor;
public bool bInheritHandle;
}
[DllImport("kernel32.dll", EntryPoint = "CloseHandle", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
public extern static bool CloseHandle(IntPtr handle);
[DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
public extern static bool CreateProcessAsUser(IntPtr hToken, String lpApplicationName, String lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes,
ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandle, int dwCreationFlags, IntPtr lpEnvironment,
String lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
[DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")]
public extern static bool DuplicateTokenEx(IntPtr ExistingTokenHandle, uint dwDesiredAccess,
ref SECURITY_ATTRIBUTES lpThreadAttributes, int TokenType,
int ImpersonationLevel, ref IntPtr DuplicateTokenHandle);
string curFile2 = AppDomain.CurrentDomain.BaseDirectory + "OnStart.txt";
public void createProcessAsUser()
{
IntPtr Token = new IntPtr(0);
IntPtr DupedToken = new IntPtr(0);
bool ret;
//Label2.Text+=WindowsIdentity.GetCurrent().Name.ToString();
SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
sa.bInheritHandle = false;
sa.Length = Marshal.SizeOf(sa);
sa.lpSecurityDescriptor = (IntPtr)0;
Token = WindowsIdentity.GetCurrent().Token;
const uint GENERIC_ALL = 0x10000000;
const int SecurityImpersonation = 2;
const int TokenType = 1;
ret = DuplicateTokenEx(Token, GENERIC_ALL, ref sa, SecurityImpersonation, TokenType, ref DupedToken);
if (ret == false)
File.AppendAllText(curFile2, "DuplicateTokenEx failed with " + Marshal.GetLastWin32Error());
else
File.AppendAllText(curFile2, "DuplicateTokenEx SUCCESS");
STARTUPINFO si = new STARTUPINFO();
si.cb = Marshal.SizeOf(si);
si.lpDesktop = "";
string Path;
Path = #"C:\myEXEpath";
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
ret = CreateProcessAsUser(DupedToken, Path, null, ref sa, ref sa, false, 0, (IntPtr)0, "c:\\", ref si, out pi);
if (ret == false)
File.AppendAllText(curFile2, "CreateProcessAsUser failed with " + Marshal.GetLastWin32Error());
else
{
File.AppendAllText(curFile2, "CreateProcessAsUser SUCCESS. The child PID is" + pi.dwProcessId);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
ret = CloseHandle(DupedToken);
if (ret == false)
File.AppendAllText(curFile2, Marshal.GetLastWin32Error().ToString() );
else
File.AppendAllText(curFile2, "CloseHandle SUCCESS");
}
The steps you outlined above will generate one process per execution of the method createProcessAsUser(). Now this method does not contain any code to terminate or kill the process so repeatidly calling this method will generate more than one process. As your code is displayed the method will inface generate only one process.
I think the real answer is how are you calling this method. As you stated in the comment
I'm trying to launch the .exe in the user session
I can only assume you may be calling this process from the Session start, Application_BeginRequest or another method that may be executed multiple times depending on how your application is designed (the calling code for this method would be great as an edit).
As I stated earlier the exe is being executed every time the method is called and not terminated. If you only ever want one instance of the application running you will have to examine the process tree to identify if the process is already running. Now if you should have one process running per user you will need to do the above but also maintain a reference the process ID that was created the first time the application started.
Review the code below for the changes (simplified)
public void createProcessAsUser()
{
//one process per session
object sessionPID = Session["_servicePID"];
if (sessionPID != null && sessionPID is int && Process.GetProcessById((int)sessionPID) != null)
return; //<-- Return process already running for session
else
Session.Remove("_servicePID");
//one process per application
object applicationPID = Application["_applicationPID"];
if (applicationPID != null && applicationPID is int && Process.GetProcessById((int)applicationPID) != null)
return; //<-- Process running for application
else
Application.Remove("_applicationPID");
//omitted starting code
if (ret == false)
// omitted log failed
else
{
// omitted log started
//for one process per session
Session["_servicePID"] = Convert.ToInt32(pi.dwProcessId);
//for one process per application
Application["_applicationPID"] = Convert.ToInt32(pi.dwProcessId);
//close handles
}
// omitted the rest of the method
}
This simple saves a reference to the Process ID that was created for the application into either the Session state for one process per user or the Application state for one process per application instance.
Now if this is the intended result you may also want to look at Terminating the process either when the application shutdown (gracefully) or the session ends. That would be very similar to our first check but can be done as seen below. *note this doesn't take into account the worker process shutting down without calling the session \ application end events those should be handled as well possibly in the application start.
//session end
void Session_End(object sender, EventArgs e)
{
object sessionPID = Session["_servicePID"];
if (sessionPID != null && sessionPID is int)
{
Process runningProcess = Process.GetProcessById((int)sessionPID);
if (runningProcess != null)
runningProcess.Kill();
}
}
//application end
void Application_End(object sender, EventArgs e)
{
object applicationPID = Application["_applicationPID"];
if (applicationPID != null && applicationPID is int && Process.GetProcessById((int)applicationPID) != null)
{
Process runningProcess = Process.GetProcessById((int)applicationPID);
if (runningProcess != null)
runningProcess.Kill();
}
}
Again, back to the original question how do you stop the multiple instances. The answer is simply stop the ability to spawn multiple instances by examining how you start the instances (I.e. the calling code to the method createProcessAsUser()) and adjust your method accordingly to avoid multiple calls.
Please post an edit if this inst helpful with details on how the createProcessAsUser() method is called.
Update 1:
Session \ Application does not exist in the context. This will happen if the method createProcessUser() is in a different class than an ASPX page (as it is on the tutorial).
Because of this you will need to change for the existance of an HttpContext this can simply done by calling
HttpContext.Currrent
I have adapted the method above to include checks to the HttpContext
public void createProcessAsUser()
{
//find the http context
var ctx = HttpContext.Current;
if (ctx == null)
throw new Exception("No Http Context");
//use the following code for 1 process per user session
object sessionPID = ctx.Session["_servicePID"];
if (sessionPID != null && sessionPID is int && Process.GetProcessById((int)sessionPID) != null)
return; //<-- Return process already running for session
else
ctx.Session.Remove("_servicePID");
//use the following code for 1 process per application instance
object applicationPID = ctx.Application["_applicationPID"];
if (applicationPID != null && applicationPID is int && Process.GetProcessById((int)sessionPID) != null)
return; //<-- Process running for application
else
ctx.Application.Remove("_applicationPID");
// omitted code
if (ret == false)
{
//omitted logging
}
else
{
//omitted logging
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
//for one process per session
ctx.Session["_servicePID"] = Convert.ToInt32(pi.dwProcessId);
//for one process per application
ctx.Application["_applicationPID"] = Convert.ToInt32(pi.dwProcessId);
}
//omitted the rest
}
You will not the changes are in the first few lines where it gets the current HttpContext (you must add using System.Web) by calling var ctx = HttpContext.Current
Next we just check that the ctx variable is not null. If it is null I am throwing an exception, however you can handle this anyway you wish.
From there instead of directly calling Session and Application I have changed the references to ctx.Session... and ctx.Application...
Update 2:
This is a Windows Application calling the method above. Now this changes the ball game as the code above is really meant to start a process as the impersonated windows identity. Now Impersonation is typcially done in WebApplications not WinForms (can be done though).
If you are not impersonating a different user than the user who is running the application. Meaning the user logged in is the user that is running the application. If this is so then your code becomes ALOT easier.
Below is an example of how this can be achieved.
/// <summary>
/// static process ID value
/// </summary>
static int? processID = null;
public void startProcess()
{
//check if the processID has a value and if the process ID is active
if (processID.HasValue && Process.GetProcessById(processID.Value) != null)
return;
//start a new process
var process = new Process();
var processStartInfo = new ProcessStartInfo(#"C:\myProg.exe");
processStartInfo.CreateNoWindow = true;
processStartInfo.UseShellExecute = false;
process.StartInfo = processStartInfo;
process.Start();
//set the process id
processID = process.Id;
}
Again as this is a Win Forms application you can use the Process object to launch a process, this windows application will run as the user running the Windows Forms application. In this example we also hold a static reference to the processID and check the if the processID (if found) is already running.
I have an AddIn which I want to invoke through Excel interop from a C# winforms application.
I can't get the addin etc. to load unless I uninstall and resinstall it each time (this is apparantly something to do with Excel not loading addins when you use interop - btw, can't get their example to work in C#). Unfortunately this is slow and annoying to the user so I need to streamline it.
I want to have one instance of Excel but load an already installed addin without forcing this install/reinstall problem.
I've searched and searched but everything I find on google gives the solution to install/reinstall. Is there any other way? The add-in is installed, I just want excel to load it.
This is what I am doing at the moment (taken from google'd advice):
// loop over the add-ins and if you find it uninstall it.
foreach (AddIn addIn in excel.AddIns)
if (addIn.Name.Contains("My Addin"))
addin.Installed = false;
// install the addin
var addin = excel.AddIns.Add("my_addin.xll", false);
addin.Installed = true;
After a while I found the answer hidden in strange places in the MS help: and this blog post.
That isn't all the info you need though. Things to note: you must have at least one workbook open or otherwise Excel barfs. Here's some rudementry code to get started:
var excel = new Application();
var workbook = excel.workbooks.Add(Type.Missing);
excel.RegisterXLL(pathToXll);
excel.ShowExcel();
If you want you can close the temporary workbook (if you've run some macros etc.) and remember to tidy everything up with plenty of calls to Marshal.ReleaseComObject!
It seems that you have to get the correct Excel process to work with. Use this class to open the Excel document:
class ExcelInteropService
{
private const string EXCEL_CLASS_NAME = "EXCEL7";
private const uint DW_OBJECTID = 0xFFFFFFF0;
private static Guid rrid = new Guid("{00020400-0000-0000-C000-000000000046}");
public delegate bool EnumChildCallback(int hwnd, ref int lParam);
[DllImport("Oleacc.dll")]
public static extern int AccessibleObjectFromWindow(int hwnd, uint dwObjectID, byte[] riid, ref Window ptr);
[DllImport("User32.dll")]
public static extern bool EnumChildWindows(int hWndParent, EnumChildCallback lpEnumFunc, ref int lParam);
[DllImport("User32.dll")]
public static extern int GetClassName(int hWnd, StringBuilder lpClassName, int nMaxCount);
public static Application GetExcelInterop(int? processId = null)
{
var p = processId.HasValue ? Process.GetProcessById(processId.Value) : Process.Start("excel.exe");
try
{
Thread.Sleep(5000);
return new ExcelInteropService().SearchExcelInterop(p);
}
catch (Exception)
{
Debug.Assert(p != null, "p != null");
return GetExcelInterop(p.Id);
}
}
private bool EnumChildFunc(int hwndChild, ref int lParam)
{
var buf = new StringBuilder(128);
GetClassName(hwndChild, buf, 128);
if (buf.ToString() == EXCEL_CLASS_NAME) { lParam = hwndChild; return false; }
return true;
}
private Application SearchExcelInterop(Process p)
{
Window ptr = null;
int hwnd = 0;
int hWndParent = (int)p.MainWindowHandle;
if (hWndParent == 0) throw new Exception();
EnumChildWindows(hWndParent, EnumChildFunc, ref hwnd);
if (hwnd == 0) throw new Exception();
int hr = AccessibleObjectFromWindow(hwnd, DW_OBJECTID, rrid.ToByteArray(), ref ptr);
if (hr < 0) throw new Exception();
return ptr.Application;
}
}
Use the class in your application like this:
static void Main(string[] args)
{
Microsoft.Office.Interop.Excel.Application oExcel = ExcelInteropService.GetExcelInterop();
foreach (AddIn addIn in oExcel.AddIns)
{
addIn.Installed = true;
}
}
I had a similar problem and none of the above solutions seemed to work for me. So, I finally came up with the idea of simply opening the ".xla" file as a workbook :
//I first get the path to file, in my case it was in the same dir as the .exe
var XLA_NAME = Path.Combine(Directory.GetCurrentDirectory(), ".xla");
Excel.Application xlRep = new Excel.Application();
xlRep.visible = true;
xlRep.Workbooks.Open("XLA_NAME");