Using David Brown's downloadable sample at ImplicitOperator I've put together an often working GraphViz renderer of a DOT file to an in-memory image.
Unfortunately, my version fails at a guestimated rate of 1 in 8 executions from with the IIS 7 ASP.NET web application I've got it in. I know that the DOT file data is consistent because I've compared the failing instances against the working instances and they are identical.
As David's site seems to suggest that the blog's future is uncertain, I'll reprint the interop pieces here. Hope he doesn't mind. The failure is toward the end of the sample, within RenderImage at the third statement set. I've noted the failing line with // TODO: .... The failure always happens there (if it happens at all). By this line, g and gvc pointers are non-zero and the layout string is correctly populated.
I don't really expect anyone to debug this at runtime. Rather, I hope that some static analysis of the interop code might reveal the problem. I can't think of any advanced marshaling techniques available here - two IntPtrs and a string shouldn't need a lot of help, right?
Thanks!
Side note: I've looked at a trial of MSAGL and I'm not impressed - for $99 from Microsoft, I'd expect more features for node layout and/or documentation explaining what I'm missing. Maybe my rapid port from QuickGraph to AGL unfairly biases my experience because of some fundamental differences in the approaches (edge-centric vs node-centric, for example).
public static class Graphviz
{
public const string LIB_GVC = "gvc.dll";
public const string LIB_GRAPH = "graph.dll";
public const int SUCCESS = 0;
/// <summary>
/// Creates a new Graphviz context.
/// </summary>
[DllImport(LIB_GVC)]
public static extern IntPtr gvContext();
/// <summary>
/// Releases a context's resources.
/// </summary>
[DllImport(LIB_GVC)]
public static extern int gvFreeContext(IntPtr gvc);
/// <summary>
/// Reads a graph from a string.
/// </summary>
[DllImport(LIB_GRAPH)]
public static extern IntPtr agmemread(string data);
/// <summary>
/// Releases the resources used by a graph.
/// </summary>
[DllImport(LIB_GRAPH)]
public static extern void agclose(IntPtr g);
/// <summary>
/// Applies a layout to a graph using the given engine.
/// </summary>
[DllImport(LIB_GVC)]
public static extern int gvLayout(IntPtr gvc, IntPtr g, string engine);
/// <summary>
/// Releases the resources used by a layout.
/// </summary>
[DllImport(LIB_GVC)]
public static extern int gvFreeLayout(IntPtr gvc, IntPtr g);
/// <summary>
/// Renders a graph to a file.
/// </summary>
[DllImport(LIB_GVC)]
public static extern int gvRenderFilename(IntPtr gvc, IntPtr g,
string format, string fileName);
/// <summary>
/// Renders a graph in memory.
/// </summary>
[DllImport(LIB_GVC)]
public static extern int gvRenderData(IntPtr gvc, IntPtr g,
string format, out IntPtr result, out int length);
public static Image RenderImage(string source, string layout, string format)
{
// Create a Graphviz context
IntPtr gvc = gvContext();
if (gvc == IntPtr.Zero)
throw new Exception("Failed to create Graphviz context.");
// Load the DOT data into a graph
IntPtr g = agmemread(source);
if (g == IntPtr.Zero)
throw new Exception("Failed to create graph from source. Check for syntax errors.");
// Apply a layout
if (gvLayout(gvc, g, layout) != SUCCESS) // TODO: Fix AccessViolationException here
throw new Exception("Layout failed.");
IntPtr result;
int length;
// Render the graph
if (gvRenderData(gvc, g, format, out result, out length) != SUCCESS)
throw new Exception("Render failed.");
// Create an array to hold the rendered graph
byte[] bytes = new byte[length];
// Copy the image from the IntPtr
Marshal.Copy(result, bytes, 0, length);
// Free up the resources
gvFreeLayout(gvc, g);
agclose(g);
gvFreeContext(gvc);
using (MemoryStream stream = new MemoryStream(bytes))
{
return Image.FromStream(stream);
}
}
}
Visual Studio 2010 added a "PInvokeStackImbalance" detection that I think helped me fix the problem. While the image would still get generated, I would get this error several times.
By specifying CallingConvention = CallingConvention.Cdecl on all the LIBGVC PInvoke sigantures, the error and crashes disappear.
[DllImport(LIB_GVC, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr gvContext();
[DllImport(LIB_GVC, CallingConvention = CallingConvention.Cdecl)]
public static extern int gvFreeContext(IntPtr gvc);
...
I've had no crashes since making this change, so I'll mark this as the new answer, for now.
I remember running into problems like this while I was working on the article and posted questions about them here and here (the second of which you appear to have commented on; my apologies for not seeing the comment earlier).
The first question is probably not directly related to this because I was writing a test application in C, not C#, and gvLayout was failing every single time instead of just every now and then. Regardless, make sure your application does have access to the Graphviz configuration file (copy it alongside your executable or place the Graphviz bin directory in your system PATH).
The second question is more relevant, except it applies to agmemread and not gvLayout. However, it's very possible that both are caused by the same issue. I was never able to figure out a solution, so I sent the Graphviz team a bug report. Unfortunately, it hasn't been resolved.
The Graphviz API is very simple, so it's unlikely that the issue is caused by the interop code. There is one thing that I neglected to mention in the article: the result pointer needs to be freed. I don't know if this will fix your issue, but it's still a good idea to add it anyway:
[DllImport("msvcrt.dll", SetLastError = true)]
private static extern void free(IntPtr pointer);
// After Marshal.Copy in RenderImage
free(result);
As far as I know, this issue is related to how Graphivz recovers from internal errors, so until the bug is addressed, I'm not sure there's anything you or I can do. But, I'm not an interop expert, so hopefully someone else can help you out a little more.
changing the calling convention DOESN'T help!!
Yes, it works when a simple (actually not that simple) dot source, but it ALWAYS crash if the dot source containing unicode character (simplified chinese).
Related
I am writing a .Net CE application on a Smart Device that has a printer on it. I collect my data in a StringBuilder object, and then try to print it. Here's how I print it
var receipt = new StringBuilder();
// ...
Printer.getInstance().print(receipt.ToString(), (int) Printer.TextAlign.Left, 0, 24, false);
Printer class is imported from a DLL. The application throws an unmanaged exception on the print line and crashes. But when I change my code to this
var receipt = new StringBuilder();
// ...
var str = receipt.ToString();
Printer.getInstance().print(str, (int) Printer.TextAlign.Left, 0, 24, false);
everything works fine. How is it even possible for the evalution of the StringBuilder affect the flow?
Here's a the methods from my Printer.dll (decompiled)
public int print(string text, int textAlign, int fontWeight, int fontSize, bool endLineFeed)
{
open();
Int32 prnReturn;
Printer.Prn_SetLang(1);
Printer.PRN_SetFont((byte) fontSize, (byte) fontSize, 0);
Printer.PRN_SetAlign(textAlign);
Printer.PRN_SetBold(fontWeight);
Prn_String(text.TrimEnd());
prnReturn = PRN_PrintAndWaitComplete();
if(endLineFeed)
printEndingLineFeed();
close();
return prnReturn;
}
public void open()
{
PRN_Open();
}
public void close()
{
PRN_Close();
}
private void printEndingLineFeed()
{
open();
//lineFeed(ENDING_LINE_FEED);
PRN_FeedLine(ENDING_LINE_FEED);
close();
}
and here are the methods that it calls from another DLL. Unfortunately, DotPeek doesn't decompile this.
[DllImport(PrinterDllName, SetLastError = true, EntryPoint = "PRN_FeedLine")]
public static extern Int32 PRN_FeedLine(Int32 pszData);
[DllImport(PrinterDllName, SetLastError = true, EntryPoint = "PRN_PrintAndWaitComplete")]
public static extern Int32 PRN_PrintAndWaitComplete();
Edit: Thanks to Kevin Gosse, I found out that the problem is only there in Debug mode. So my question now is, how does the debug mode evaluation differ from normal execution. Although I do understand that this might be off-topic, I'd be glad if someone could share a relevant piece of documentation.
In release mode, both versions of your code are identical.
In debug mode, there is a subtle difference as the lifetime of str will be extended until the end of the current method (for debugging purpose).
So this code in debug mode:
var receipt = new StringBuilder();
// ...
var str = receipt.ToString();
Printer.getInstance().print(str, (int) Printer.TextAlign.Left, 0, 24, false);
Is equivalent to this in release mode:
var receipt = new StringBuilder();
// ...
var str = receipt.ToString();
Printer.getInstance().print(str, (int) Printer.TextAlign.Left, 0, 24, false);
GC.KeepAlive(str);
When your string is given to the native code, it's not tracked by the GC anymore. Therefore, it could be collected which will cause errors in the native part.
In theory, the marshaller automatically protects you from such situations when using common types (such as string), as described here: https://learn.microsoft.com/en-us/dotnet/framework/interop/copying-and-pinning?redirectedfrom=MSDN. That's another puzzle, but it's possible that .net compact framework has a different marshaller and doesn't protect you automatically. Unfortunately it's hard to find specific documentation on the subject.
What surprised me when you decompiled the method is how the native call doesn't actually receive the original string but text.TrimEnd(). It means that the lifetime of the original value shouldn't have any impact (since the native code receives a different string). However, it turns out that .TrimEnd returns the original string when there's nothing to trim. When you added a space, it started crashing even with the version of the code that extends the lifetime of the string. That's because now you're extending the lifetime of the wrong string (since TrimEnd() will return a different instance, and that's the one that will be used by the native code).
I believe that the code working in Release is pure luck. Maybe it just changes the timing of the garbage collector and you don't run into that specific issue, but it could cause problems in the future. Out of caution, I would suggest you to:
Trim the string before calling Printer.getInstance().print
Call GC.KeepAlive on the trimmed string after the call to print
If you want to be extra safe, you can pin the string instead of calling GC.KeepAlive
I wish I could give more than theories, but I believe you're running into specificities of .net compact framework. If somebody with more experience on the subject reads this and could give more information, that would be much appreciated.
Background:
I have a requirement to create a dimming effect on another monitor. I think I solved it by using a WPF Window that takes up the entire screen dimensions with Topmost and AllowsTransparency = True. It has an inner black glow effect and has the style WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW applied to it (among other things) to allow users to click through to the apps behind it.
I monitor for EVENT_OBJECT_REORDER events in Windows and call SetWindowPos to force the Topmost state above other Topmost windows. It seems to work well so far in my proof of concept testing.
The problem I found was this dimming (window) would cover the task bar, but not if I click the Start Menu. I'm currently testing with Windows 10. If I click the Start Menu, it causes the Start Menu and Taskbar to appear above the dimming (window). I wanted everything to remain dim, always.
I solved this issue by setting uiAccess=true in the app manifest, generating a self-signed cert, and copying the exe over to "c:\program files*". This allows me to force a Topmost state for my window, even above the Start Menu.
My questions:
Is there a way to position a window over the Start Menu without uiAccess? Or even another way to force dimness to a screen without using a window (but not dependent on monitor drivers or hardware capabilities)?
If not, what considerations do I need to keep in mind when distributing a WPF app (via a WiX setup project or something similar) that is to bypass UIPI restrictions with uiAccess=True? Can I simply install my self signed cert during the setup process? Will the user run into any additional hurdles? Will I, as a developer, run into any additional hurdles while building this (aside from what I've already mentioned)?
Thank you!
I monitor for EVENT_OBJECT_REORDER events
You are using SetWinEventHook(). This scenario fails the classic "what if two programs do this" bracket. Raymond Chen discussed this pretty well in this blog post, giving your approach a dedicated post.
This is a lot more common than you might assume. Every Windows machine has a program that does this for example, run Osk.exe, the on-screen keyboard program. Interesting experiment, I predict it will flicker badly for a while but assume it will eventually give up. Not actually sure it does, last time I tried this was at Vista time and it wouldn't, please let us know.
Fairly sure you will conclude that this isn't the right way to go about it so uiAccess is moot as well. You needed it here to bypass UIPI and make SetWindowPos() work. An aspect of UAC that blocks attempts by a program to hijack an elevated program's capabilities. Covering the Start window qualifies as a DOS attack. Bigger problem here is that your self-signed certificate isn't going to work, you'll have to buy a real one. Sets you back several hundred dollars every ~7 years.
Controlling monitor brightness with software isn't that easy to do correctly. Everybody reaches for SetDeviceGammaRamp() and that is what you should do as well. The MSDN docs will give you plenty of FUD but afaik every mainstream video adapter driver implements it. It was popular in games. One unavoidable limitation is that it is only active for the desktop in which your program runs. So not for the secure desktop (screen saver and Ctrl+Alt+Del) and not for other login sessions unless they start your program as well.
WMI is too flaky to consider. Not so sure why it fails so often, I assume it has something to do with the often less-than-stellar I2C interconnect between the video adapter and the monitor. Or laptops that want to control brightness with an Fn keystroke, that feature always wins. Or the Windows feature that automatically adjusts brightness based on ambient light, invariably the more desirable way to do this and a hard act to follow.
Most common outcome is likely to be a shrug at your program and a curse of the user at the clumsy monitor controls. But he'll fiddle with it and figure it out. Sorry.
This won't answer anything about uiAccess=true, but...
Dimming the Screen
As an alternative way to dim the screen, you could try using SetDeviceGammaRamp to dim all screens at once (if that's desired).
For example, take the following helper class:
/// <summary> Allows changing the gamma of the displays. </summary>
public static class GammaChanger
{
/// <summary>
/// Retrieves the current gamma ramp data so that it can be restored later.
/// </summary>
/// <param name="gamma"> [out] The current gamma. </param>
/// <returns> true if it succeeds, false if it fails. </returns>
public static bool GetCurrentGamma(out GammaRampRgbData gamma)
{
gamma = GammaRampRgbData.Create();
return GetDeviceGammaRamp(GetDC(IntPtr.Zero), ref gamma);
}
public static bool SetGamma(ref GammaRampRgbData gamma)
{
// Now set the value.
return SetDeviceGammaRamp(GetDC(IntPtr.Zero), ref gamma);
}
public static bool SetBrightness(int gamma)
{
GammaRampRgbData data = new GammaRampRgbData
{
Red = new ushort[256],
Green = new ushort[256],
Blue = new ushort[256]
};
int wBrightness = gamma; // reduce the brightness
for (int ik = 0; ik < 256; ik++)
{
int iArrayValue = ik * (wBrightness + 128);
if (iArrayValue > 0xffff)
{
iArrayValue = 0xffff;
}
data.Red[ik] = (ushort)iArrayValue;
data.Green[ik] = (ushort)iArrayValue;
data.Blue[ik] = (ushort)iArrayValue;
}
return SetGamma(ref data);
}
[DllImport("gdi32.dll")]
private static extern bool SetDeviceGammaRamp(IntPtr hdc, ref GammaRampRgbData gammaRgbArray);
[DllImport("gdi32.dll")]
private static extern bool GetDeviceGammaRamp(IntPtr hdc, ref GammaRampRgbData gammaRgbArray);
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hWnd);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct GammaRampRgbData
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
public UInt16[] Red;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
public UInt16[] Green;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
public UInt16[] Blue;
/// <summary> Creates a new, initialized GammaRampRgbData object. </summary>
/// <returns> A GammaRampRgbData. </returns>
public static GammaRampRgbData Create()
{
return new GammaRampRgbData
{
Red = new ushort[256],
Green = new ushort[256],
Blue = new ushort[256]
};
}
}
}
Combined with the following in a static void Main(), and the program will change the brightness until the user exits the application:
GammaChanger.GammaRampRgbData originalGamma;
bool success = GammaChanger.GetCurrentGamma(out originalGamma);
Console.WriteLine($"Originally: {success}");
success = GammaChanger.SetBrightness(44);
Console.WriteLine($"Setting: {success}");
Console.ReadLine();
success = GammaChanger.SetGamma(ref originalGamma);
Console.WriteLine($"Restoring: {success}");
Console.ReadLine();
Do note however, that this is applying a global solution to a local problem
If you do go this route, I'd suggest really making sure that you're restoring the user's gamma before exiting, otherwise they'll be left with a less than steller experience that your app crashed and the screen is no permanently dimmed.
Sources:
Discussion of the usage of SetGammaRamp, which is where a bulk of the algorithm comes from
An alternative implementation of the above solution.
I have a third-party Windows app that supports a C plugin/driver (see spec below) to be a passive data receiver once initialized. Now I found a C# package that does the data gathering and Pushing. The only missing piece here is a bridge between the C dll and the C# dll.
The data flow is like this: When the app is launched, it loads and calls the C dll which contains several exported functions including an init function. In this init function, I like to establish a call to the C# to setup some network connection and prepare for incoming data. Once that done, according to the driver spec, the C# dll will gather data and stream it to the receiving driver. To accommodate this, I have two thoughts (you may come up with more):
1) to wrap the C# with C++/Cli and call the expose C#-like methods from the driver. Declare an object with gcroot, then instantiate it with gcnew then call the method(s). Tried that and I got stackoverflowexception. I am new to this mix-mode programming and can't figure out why that happened.
2) to wrap the C dll in some way (like using C++/Cli to import the C functions then interact with the C# data streamer) to be callable from C#. What is the best way to do this?
I have done some reading and it seems C++/Cli is the easy route but I am open to other not so complicated options as well. What are the project settings I have to add/modify to make it work should I choose C++/Cli or any way you suggest?
As I am new to tackle this kind of problem, any samples or related links are helpful. So I appreciate if you could demonstrate how things work one way or the other.
Here is piece of the skeletal C# dll referenced (other methods are omitted):
public class Client
{
public Client()
{
//reset();
}
public void test()
{
Console.WriteLine("test");
}
/// <summary>
/// Connect connects the Client to the server.
/// Address string has the form host:port.
/// </summary>
public void Connect(string address)
{
Disconnect();
// validate address
int sep = address.IndexOf(':');
if (sep < 0)
{
throw new ArgumentException("Invalid network address");
}
// set host and port
host = address.Substring(0, sep);
toPort(ref port, address.Substring(sep + 1));
// connect
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(host, port);
rw.Set(socket, CLIENT_DEFAULT_BUFFER_SIZE);
}
/// <summary>
/// Disconnect disconnects the Client from the server.
/// </summary>
public void Disconnect()
{
backlog.Clear();
try
{
if (Connected)
{
write("close");
}
}
catch (Exception e)
{
}
//reset();
//rw.Close();
}
}
Here is the skeletal spec of the C dll:
//APIs
#ifdef __cplusplus
extern "C"{
#endif
#define DLL __declspec(dllexport)
////////////////////////////////////////////////////////////////////////////
// Initialization: do some prep for receiving data from C#
// params:
// hWnd handle
// Msg message
// nWorkMode work mode
// return:
// 1 true
// -1 false
DLL int Init(HWND hWnd, UINT Msg, int nWorkMode);
// Quitting and closing
// Param:
// hWnd same handle as Init
// return:
// 1 true
// -1 fals
DLL int Quit(HWND hWnd);
#ifdef __cplusplus
}
#endif
To call C functions in an external DLL, you can use C++/CLI as you've already mentioned, or use P/Invoke (Platform Invoke).
With P/Invoke, you define a static extern method in your C# assembly then decorate it with appropriate attributes. After that, you can call the method as per any other C# method, and the .NET Framework plumbing will handle the loading of the DLL and marshalling the method parameters back and forth.
For example:
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SystemParametersInfo(uint uiAction, uint uiParam, uint pvParam, uint fWinIni);
The method is declared with the static and extern keywords, and decorated with the DllImport attribute which identifies the DLL that contains the C function. That's generally the minimum you will need.
The hardest part is determining which C types map to which .NET types for the method parameters and return type: this is a bit of a black art! If you visit pinvoke.net you can see how the Windows APIs have been translated, which may help. Searching the web for "pinvoke" should also turn up a number of useful resources.
How to detect if screen reader is running (JAWS)?
As I understand in .NET 4 we can use AutomationInteropProvider.ClientsAreListening from System.Windows.Automation.Provider namespace, but what if I have to do it for .NET 2.0?
I tried to inspect ClientsAreListening source code, it calls external RawUiaClientsAreListening method from UIAutomationCore.dll library.
Do you have any ideas how to implement JAWS detection in .NET 2.0?
Use the SystemParametersInfo function passing a uiAction of SPI_GETSCREENREADER.
You will need to use P/Invoke for this, for example:
internal class UnsafeNativeMethods
{
public const uint SPI_GETSCREENREADER = 0x0046;
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SystemParametersInfo(uint uiAction, uint uiParam, ref bool pvParam, uint fWinIni);
}
public static class ScreenReader
{
public static bool IsRunning
{
get
{
bool returnValue = false;
if (!UnsafeNativeMethods.SystemParametersInfo(UnsafeNativeMethods.SPI_GETSCREENREADER, 0, ref returnValue, 0))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "error calling SystemParametersInfo");
}
return returnValue;
}
}
}
This is possibly better than using the ClientsAreListening property as this property appears to return true for any automation client, not just screen readers.
Also see:
Using SystemParametersInfo from C# (SPI_GETSCREENREADER SPI_SETSCREENREADER) (Stack Overflow)
INFO: How Clients and Servers Should Use SPI_SETSCREENREADER and SPI_GETSCREENREADER (Microsoft KB)
You should also listen for the WM_SETTINGCHANGE message to detect if a screen reader starts / stops running.
Update (in response to BrendanMcK's comments):
Although this is never explicitly documented in as many words, looking at the description of the flag I think the purpose of this flag is relatively clear:
Determines whether a screen reviewer utility is running. A screen reviewer utility directs textual information to an output device, such as a speech synthesizer or Braille display. When this flag is set, an application should provide textual information in situations where it would otherwise present the information graphically.
What this is saying is that applications set this flag whenever an application wishes the UI to behave as if a screen reader is running, regardless of whether or not that application is actually a screen reader or not.
Suitable things to do in response to this flag is to add text in order to "read" otherwise intuitive UI state to the user. If radical changes are needed to make your UI screen reader accessible then the chances are that your UI also isn't that intuitive to sigted users and could probably do with a re-think.
I'm using System.Diagnostics.Process.Start to launch a remote application on another domain machine.
Unfortunately, if the remote process is already running, but in the background of the remote machine's desktop, the application does not gain focus using Process.Start.
Question 1: Is there another API or mechanism to force the remote application to gain focus, or flash to get the user's attention?
The other issue I noticed, is if the remote process is already running, a new instance may be executed in addition to the original. This violates MSDN's documentation which says:
"If the process is already running, no additional process resource is started. Instead, the existing process resource is reused and no new Process component is created. In such a case, instead of returning a new Process component, Start returns null to the calling procedure."
Question 2: Has anyone found a way to prevent a second instance of the application from launching in this case? Is WMI a better choice to use for remote launching of applications?
Well, don't know how well this will work for you, but it is an example class that you could use in the helper program. This is only the start, if you plan on using it, you will need a networking system (not to bad with C# though). Tell me how it works for you.
/// <summary>
/// Allows you to start a specified program, or if it is already running, bring it into focus
/// </summary>
static class SFProgram
{
static public void StartFocus(string FileName, string ProcessName)
{
if (!ProcessStarted(ProcessName))
Process.Start(FileName);
else
SFProgram.BringWindowToTop("notepad");
}
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
/// <summary>
/// Bring specified process to focus
/// </summary>
/// <param name="windowName">Process Name</param>
/// <returns>If it was successful</returns>
private static bool BringWindowToTop(string windowName)
{
Process[] processes = Process.GetProcessesByName(windowName);
foreach (Process p in processes)
{
int hWnd = (int)p.MainWindowHandle;
if (hWnd != 0)
{
return SetForegroundWindow((IntPtr)hWnd);
}
//p.CloseMainWindow();
}
return false;
}
private static bool ProcessStarted(string ProcessName)
{
Process[] processes = Process.GetProcessesByName(ProcessName);
return (processes.Length > 0);
}
}