I support an old VS 2010 C++ Video Application. This application calls Video Integrators that are written in C# 2015 (and older versions as well). We have run into a unique issue that I have pinpointed to being the transfer of a camera list from the Integrator to the Video App, where the string of data is truncated at exactly 5087 characters. (resulting in only 88 of the 134 cameras being loaded into the Video Application) I am not strong at C++ at all, so I'm not entirely certain what other options I have for data objects and definition. I totally presume the issue is on the C++ side, because I can see the complete outgoing list of cameras in the C# Integrator code. (but maybe I'm mistaken)
//Video Application code (c++ 2010)
deviceLibrary.h file
//get setting extended
typedef TCHAR* (*DI_GETSETTINGEX)(HWND hParentWnd, int nParameter,LPARAM lParam);
#define DI_GETSETTINGEX_ENTRY "GetSettingEx"
class CDeviceLibrary : public CCameraWindow
{
public:
DI_GETSETTINGEX m_pGetSettingEx;
};
deviceLibrary.cpp
CDeviceLibrary::CDeviceLibrary(int nId,LPCTSTR pszName,LPCTSTR pszView,HINSTANCE hLib,HWND hWnd,LPCTSTR pszLibrary,LPCTSTR pszPropertiesString)
{
m_pGetSettingEx = (DI_GETSETTINGEX)GetProcAddress(m_hLib,DI_GETSETTINGEX_ENTRY);
}
CString CDeviceLibrary::GetSetting(int nSetting,LPARAM lParam)
{
if(m_pGetSettingEx)
return m_pGetSettingEx(AfxGetMainWnd()->m_hWnd,nSetting,lParam);
else if(m_pGetSetting)
return m_pGetSetting(nSetting,lParam);
return _T("");
}
ConfigureServers.cpp
void CConfigurationServers::LoadCameras()
{
//RIGHT HERE! THE VALUE OF strCameras only contains 5087 characters in it, when it is supposed to contain over 7000
CString strCameras = GetSetting(ParameterGetCameraList,(LPARAM)(LPCTSTR)strParameters);
}
Video Integrator Code Base (C# 2010, .NET 4.0)
public virtual String GetLibrarySetting(int Parameter,int lParam)
{
return GetCameraList(Marshal.PtrToStringAnsi((IntPtr)lParam));
}
virtual public String GetCameraList()
{
String CameraList = "";
try
{
if(!Connected)
Connect();
if(DeviceList.Count > 0)
{
List<BaseCamera>.Enumerator Cameras = DeviceList.GetEnumerator();
while(Cameras.MoveNext())
{
BaseCamera Camera = Cameras.Current;
int FeatureFlags = (int)CameraFlags.FLAG_PTZ_SUPPORT | (Camera.PTZ?(int)CameraFlags.FLAG_PTZ:0) |
(int)CameraFlags.FLAG_AUDIO_SUPPORT | (Camera.AudioIn?(int)CameraFlags.FLAG_AUDIO:0) |
(int)CameraFlags.FLAG_MIKE_SUPPORT |
(Camera.RecordNotSupported ? (int)CameraFlags.FLAG_NO_RECORD_SUPPORT | (int)CameraFlags.FLAG_NO_RECORD : 0);
CameraList += String.Format("{0};{1};{2}\n", Camera.Title.Length > 0 ? Camera.Title : Camera.Name, Camera.CameraId, FeatureFlags);
}
}
}
catch(Exception e)
{
LogException(e.Message);
}
return CameraList;
}
I am not the original creator of either of these applications. If you ask me a "why did you do...." question, I have no idea what the answer is. If any of you know of a solution that can seamlessly replace this solution, and retrieve me the full string I am looking for, wonderful. If you see the reason why I only a subset of the original list of characters is being received into the Video App, please let me know why. Thanks!
NOTE: I have stripped out hundreds of lines of code in this post. The code base is massive.
Sample text being passed, and how it is being truncated at the C++ property:
C001-P100--1-SeeTec (Peliquin);4611967493405284623;2293760
C001-P100--SeeTec (Peliquin);4611967493405284639;2293760
C002-P100--1-SeeTec (Peliquin);4611967493405284632;2293760
C002-P100--SeeTec (Peliquin);4611967493405284646;2293760
C003-P100--1-SeeTec (Peliquin);4611967493405284660;2293760
C003-P100--SeeTec (Peli"
Arg... as it turns out, C++ cannot (at least in our app) directly call a C# application. It needs an app in the middle to do some sort of conversion between the two languages. I tracked down a tiny c++ application in our system that is referenced by the program that makes the two languages talk to each other, which had the following code in it:
wchar_t* ConvertToCharW(String^ Input)
{
static wchar_t ch[5001]=L""; //TODO: make dynamic so no String^ length limit
try
{
ch[0] = ch[5000] = '\0';
// Pin memory so GC can't move it while native function is called
pin_ptr<const wchar_t> wch = PtrToStringChars(Input);
wcscpy_s(ch,5000,wch);
}
catch(...)
{
}
return ch;
}
And there is my 5000 character limit. Since I have no idea how to make it dynamic in C++, I changed it to 20000.
Related
How do I enumerate NVMe (M2) drives an get their temperature in c# ?
It's not accessible through WMI usual queries.
There this MSFT reference to do this in c, but it's rather obscure, code is not complete:
https://learn.microsoft.com/en-us/windows/desktop/fileio/working-with-nvme-devices#temperature-queries
This answer is as much a tutorial as answer. I am a newbie to C++ and had to learn by mistakes and a lot of research to understand the range of environment related steps necessary to get code compiled and working. Along the way Visual Studio 2022 implementation of C++ has lots of quirks that I haven’t yet figured out including Functions referenced in Header Files not being found even when the Header file was properly referenced, loaded and accessible.
I have written a desktop utility program in VB which shows graphically the amount of space used on Drives as well as R/W data rates, graphs for CPU/Thread usage, Memory info, clocks, temperatures etc plus a desktop calculator and units converter. Most of the data is extracted through inbuilt PerformanceCounters except CPU/Thread Temperature which thanks to CoreTemp can be read directly from their free App when running.
Why in VB? Because it is so easy to write GUI type apps on forms with controls and manage strings and this makes up 95% of the code, with data access being the other 5%. But VB has its limitations hence C++. (Don’t wish to fire up C experts who have a thousand reasons why not to use VB)
In VB I am able to extract Drive Temperatures for SATA drives using WMI functions which adhere to S.M.A.R.T protocol, but NVMe drives don’t follow this protocol. So have implemented code as suggested by Microsoft in C++ to access NVMe drive temperatures and built it as a DLL which can be accessed by either C or VB client programs. VB does not have a Malloc memory allocation function (memset in VS) so working with unmanaged memory is more difficult, even though research has suggested it can be done. But this C++ Function is a very small amount of code, simple, fast and scanning 5 drives for temperature takes around 3ms. By comparison using WMI to read 3 SATA drives takes typically 100ms.
This is specifically written in VB Studio 2022 environment using DLL template, with the project called ReadNVMeTemperatures.
Output is a file ReadNVMeTemperatures.dll and I am happy to make this available to anyone if you don’t want to build it yourself.
Firstly the C++ files for ReadNVMeTemperatures Project
//pch.h
#pragma once
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <Windows.h>
#include "pch.h"
#include <nvme.h>
#include <winioctl.h>
#include <string>
#include <ntddstor.h>
#include <corecrt_malloc.h>
// Main.h
#pragma once
namespace GetTemp
{
class MyGetTemps
{
public:
// Receives integer a as disk number from client and returns Temperature as GetNVMeTemp
static __declspec(dllexport) int GetNVMeTemp(int a);
};
}
//main.cpp
#include "pch.h"
#include <stdexcept>
#include "main.h"
using namespace winrt;
using namespace Windows::Foundation;
using namespace std;
namespace GetTemp
{
int MyGetTemps::GetNVMeTemp(int a)
{
// a is the physical disk number and must be added to string literal below to form a string constant TargetDrive
//if only 1 known drive eg 2 to be scanned can use constexpr auto TargetDrive = L"\\\\.\\PhysicalDrive2";
std::string stddrivestring = "\\\\.\\PhysicalDrive" + to_string(a);
std::wstring widedrivestring = std::wstring(stddrivestring.begin(),
stddrivestring.end());
const wchar_t* TargetDrive = widedrivestring.c_str();
HANDLE hDevice = INVALID_HANDLE_VALUE; // handle to the drive to be examined
hDevice = CreateFileW((LPWSTR)TargetDrive, // drive to open
0, // no access to the drive
FILE_SHARE_READ | // share mode
FILE_SHARE_WRITE,
NULL, // default security attributes
OPEN_EXISTING, // disposition
FILE_FLAG_OVERLAPPED, // file attributes
NULL); // do not copy file attributes
if (hDevice == INVALID_HANDLE_VALUE) // cannot open the drive
{
return (FALSE);
}
BOOL result = 0;
PVOID buffer = NULL;
ULONG bufferLength = 0;
ULONG returnedLength = 0;
SHORT mytemp0 = 0;
SHORT mytemp1 = 0;
INT mytemp = 0;
PSTORAGE_PROPERTY_QUERY query = NULL;
PSTORAGE_PROTOCOL_DATA_DESCRIPTOR protocolDataDescr = NULL;
// Allocate buffer for use.
bufferLength = FIELD_OFFSET(STORAGE_PROPERTY_QUERY, AdditionalParameters) + sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA) + NVME_MAX_LOG_SIZE;
buffer = malloc(bufferLength);
if (buffer == NULL) {
// printf("DeviceNVMeQueryProtocolDataTest: allocate buffer failed");
return 0;
}
// Initialize query data structure to get Identify Controller Data.
memset(buffer, 0, bufferLength);
query = (PSTORAGE_PROPERTY_QUERY)buffer;
protocolDataDescr = (PSTORAGE_PROTOCOL_DATA_DESCRIPTOR)buffer;
protocolData = (PSTORAGE_PROTOCOL_SPECIFIC_DATA)query->AdditionalParameters;
query->PropertyId = StorageDeviceProtocolSpecificProperty;
query->QueryType = PropertyStandardQuery;
protocolData->ProtocolType = ProtocolTypeNvme;
protocolData->DataType = NVMeDataTypeLogPage;
protocolData->ProtocolDataRequestValue = NVME_LOG_PAGE_HEALTH_INFO;
protocolData->ProtocolDataRequestSubValue = 0; // This will be passed as the lower 32 bit of log page offset if controller supports extended data for the Get Log Page.
protocolData->ProtocolDataRequestSubValue2 = 0; // This will be passed as the higher 32 bit of log page offset if controller supports extended data for the Get Log Page.
protocolData->ProtocolDataRequestSubValue3 = 0; // This will be passed as Log Specific Identifier in CDW11.
protocolData->ProtocolDataOffset = sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA);
protocolData->ProtocolDataLength = sizeof(NVME_HEALTH_INFO_LOG);
/ Send request down.
result = DeviceIoControl(hDevice,
IOCTL_STORAGE_QUERY_PROPERTY,
buffer,
bufferLength,
buffer,
bufferLength,
&returnedLength,
NULL
);
if (!result || (returnedLength == 0)) {
//printf(("DeviceNVMeQueryProtocolDataTest: SMART/Health Information Log failed. Error Code %d.\n"), GetLastError());
return 0;
}
// Validate the returned data.
if ((protocolDataDescr->Version != sizeof(STORAGE_PROTOCOL_DATA_DESCRIPTOR)) ||
(protocolDataDescr->Size != sizeof(STORAGE_PROTOCOL_DATA_DESCRIPTOR))) {
//printf(("DeviceNVMeQueryProtocolDataTest: SMART/Health Information Log - data descriptor header not valid.\n"));
return 0;
}
protocolData = &protocolDataDescr->ProtocolSpecificData;
if ((protocolData->ProtocolDataOffset < sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA)) ||
(protocolData->ProtocolDataLength < sizeof(NVME_HEALTH_INFO_LOG))) {
//printf(("DeviceNVMeQueryProtocolDataTest: SMART/Health Information Log - ProtocolData Offset/Length not valid.\n"));
return 0;
}
// SMART/Health Information Log Data
{
PNVME_HEALTH_INFO_LOG smartInfo = (PNVME_HEALTH_INFO_LOG)((PCHAR)protocolData + protocolData->ProtocolDataOffset);
// print or return as mytemp the Temperature S.M.A.R.T. data for drive a
mytemp0 = (ULONG)smartInfo->Temperature[1] << 8;// | smartInfo->Temperature[0] - 273;
mytemp1 = smartInfo->Temperature[0];
mytemp = mytemp0 + mytemp1 - 273;
}
//printf(("Temperature Drive %d = %d deg\n"), (a), (mytemp));
return mytemp;
}
}
Before Building the final DLL solution add a DEF file to the project so that Visual Basic can find the entry point name GetNVMeTemp as defined in the main.h Header File. This is necessary as C++ adds additional characters to the entrypoint name that C++ understands but VB doesn’t.
The file is named ReadNVMeTemperatures.def i.e. the same name as the Project .def.
It is a text file and should look like this.
; ReadNVMeTemperatures.def - defines the exports for ReadNVMeTemperatures.dll
LIBRARY Test
DESCRIPTION 'A C++ dll that can be called from VB'
EXPORTS GetNVMeTemp
Here is an explanation on how to load into VS. Written by someone else. Thank You!!!
INSTRUCTIONS if you need to create a DEF file for your DLL by hand:
In the folder where your project file (.vcproj) is, create a new text file.
Rename it to "outputname.def", where "outputname" is the name of your project's output. By default, this is the name of the project, see Step 4 for clarifications.
In Visual Studio, go to Project->Properties. Go to Configuration Properties->Linker->Input and enter the name of the DEF you just created into the field named Module Definition File.
Repeat steps 1 - 3 for each configuration that outputs a different named DLL. I.e. my Debug configuration makes output_d.dll, so I need an output_d.def for Debug along with an output.def for Release.
Open all of the DEF files in your current Visual Studio editor. The format for the DEF is easy:
LIBRARY outputnameEXPORTS Function1 #1 Function2
Replace "outputname" with the name of the configuration output. As mentioned in step 4, this could be output in output.def and output_d in output_d.def.
List all of the functions you are exporting. Those functions need to be correctly exported using the correct syntax in your source files. You can manually assign an ordinal by using the #number syntax. Alternatively, you can leave it assigned automatically by not having it.
That's all there is to it. Make sure to Build->Rebuild Solution afterwards. Use Dependency Walker to verify your DLL is correctly exporting your functions with the expected names.
There is more information on this topic here: Exporting from a DLL Using DEF Files . Good luck!
Now the project can be built/compiled to create the DLL.
Next an example of a C++ Client app that will retrieve a drive temperature from this DLL. I have called it ReadNVMeTemperaturesClient and set it up in a separate project directory of the that name.
It is necessary within properties of the ReadNVMeTemperaturesClient VS project to tell VS where to find (Include Directories in C++ section and Linker section) the ReadNVMeTemperatures.dll and ReadNVMeTemperatures.lib files as well as the “main.h” header file of the DLL project.
Here is a link to the details
https://www.technical-recipes.com/2012/how-to-link-dll-files-to-c-projects/
Code for ReadNVMeTemperaturesClient Client App in C++
//pch.h
#pragma once
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
//Main.cpp
#include "pch.h"
#include <iostream>
#include "main.h" //This is the main.h of the dll project
using namespace winrt;
using namespace Windows::Foundation;
using namespace std;
int main()
{
// The physical Disk number in int a eg 2
int a = 2;
int drivetemp = 0;
drivetemp = GetTemp::MyGetTemps::GetNVMeTemp(a);
printf(("result %d \n"), (drivetemp));
return 0;
}
Next a more comprehensive VB set of routines to retrieve NVMe drive temperatures for the PC.
First search for and build an array of physical drive numbers. I use the
"Win32_LogicalDiskToPartition” class which has a member called “instancename” as this is the only WMI class I have found which returns a string containing physical disk number, partition number and logical drive character all in one string. These can be easily extracted using various string functions. There are other WMI classes (eg "Win32_Diskdrive", "MSFT_PhysicalDisk, "Win32_Volume") but they require cross referencing of results to enable logical drive to be associated with physical disk number, or in the case of a RAID array, multiple physical disk numbers per logical drive.
Example code including setting up array. Some array members I have filled using other WMI or VB Function calls. Additional members can be added to the array as needed.
Public Class PHDD
Public Property PCaption() As String 'eg ST6000DM003-2CY186
Public Property PPhysical() As Integer 'physical drive number eg 1
Public Property PNVMeType() As Boolean 'true is NVMe drive, false is SATA
Public Property PDiscSize() As Integer 'Physical Disk Size in GB
Public Property PDriveChar() As String 'eg "D"
Public Property PTemperature() As Integer 'eg 32
Public Property PInstanceName() As String 'eg SCSI\Disk&Ven_SAMSUNG&Prod_SSD_830_Series\5&36Da4fed&0&020000_0 as extracted from MSStorageDriver_ATAPISmartData used to retrieve SATA Temp from SMART
End Class
Public Class Form1
Public PDiscDrives = New Dictionary(Of Integer, PHDD)()
Public PDrivesnum as integer
Public Declare Function GetNVMeTemp Lib "ReadNVMeTemperatures.dll" (a As Integer) As Integer ‘the string constant in quotes may have to reference the exact location of this file during local VS execution. Eg “C:\myVBProject\ ReadNVMeTemperatures.dll”
Sub CreatePHDDArray()
Dim n As Integer
For n = 0 To 35
Dim MyPhdd = New PHDD()
PDiscDrives.Add(n, MyPhdd) 'create 36 array size for each physical drive...ie 10 more than max 26 Logical letters in case there are RAID arrays of Logical Drives with Multiple Disks
Next
End Sub
Sub ExtractDiskNumber
Dim devst As New ManagementClass("Win32_LogicalDiskToPartition")
Dim moct As ManagementObjectCollection = devst.GetInstances()
Dim driveinfostring as string, PDisk as integer, LChar as string, n as integer, tstring as string
Pdrivesnum = 0
For Each mot As ManagementObject In moct
If True Then
driveinfostring = mot.ToString
‘add code to extract and save Physical Disk number PDisk and Logical Drive Char LChar in the PHDD class array. Use VB “Instr”, “Substring” or “Mid” Functions to isolate and extract the values
n = InStr(t, "Disk #")
If n > 0 Then
tstring = Mid(t, n, 9) ' typically Disk #4, Partition// don't know why inbuilt Substring Function returns string starting at 1 character after n so using Mid() hear instead!!!
m = InStr(tstring, ",")
If m > 0 Then
PDisc = Val(tstring.Substring(6, m - 7)) ‘Got the Physical Disk
End If
End If
'Get the Logical Drive Char
n = InStr(t, "LogicalDisk.DeviceID=\")
If n > 0 Then
LChar = Mid(t, n + 22, 1) ' typically DeviceID=\E:\
End If
PDiskDrives.PPysical = PDisk
PDiskDrives.PDriveChar = LChar
Pdrivesnum +=1
end if
Next
End Sub
End Class
Here is an “instance” string example…
"\HOMEDN\root\cimv2:Win32_LogicalDiskToPartition.Antecedent=""\\HOMEDN\root\cimv2:Win32_DiskPartition.DeviceID=""Disk #4, Partition #1"""",Dependent=""\\HOMEDN\root\cimv2:Win32_LogicalDisk.DeviceID=""C:"""""
This is Logical Drive C, and PHYSICAL4 and the number 4 is all that is needed to get the NVMe Temperature for drive C.
Below Sub can now loop through all the PC drives from 0 to Pdrivesnum-1 and get Temperature.
Shared Sub ReadNVMeTemp()
Dim n As Integer
For n = 0 To Pdrivesnum - 1
PDiskDrives(n).PTemperature = GetNVMeTemp(PDiskDrives(n).Pphysical) 'Reads the Temperature of actualphysical disk from ReadNVMeTemperatures.dll into array
If PDiskDrives(n).PTemperature > 0 Then
PDiskDrives(n).PNVMeType = True 'set this value as true meaning the drive was NVMe, because a valid temperature was returned
Else
PDiskDrives(n).PNVMeType = False 'set as false indicating an SATA or USB drive or an error retrieving Temperature – all return a 0
End If
Next
End Sub
Hope I haven't missed any of the steps which will enable anyone who like me is still learning the C++ programming environment to get the code working. Parts of the code can be written more concisely and efficiently but I have tried to make it easy to follow. I only program for fun.
A fast implementation (but not the simplest) is to use the nvme.h winioctl.h ntddstor.h API and interop with that in c#. Here is a complete implementation.
Declaration side:
#include <nvme.h>
#include <winioctl.h>
#include <ntddstor.h>
#ifndef Pinvoke
#define Pinvoke extern "C" __declspec(dllexport)
#endif
typedef void(__stdcall *MessageChangedCallback)(const wchar_t* string);
class __declspec(dllexport) NVmeQuery final
{
public:
NVmeQuery(MessageChangedCallback managedDelegate);
~NVmeQuery();
template <class ... T>
auto LogMessage(T&& ... args) -> void;
auto GetTemp(const wchar_t* nvmePath) -> unsigned long;
MessageChangedCallback LogMessageChangedCallback{};
PNVME_HEALTH_INFO_LOG SmartHealthInfo{};
PSTORAGE_PROPERTY_QUERY query{};
PSTORAGE_PROTOCOL_SPECIFIC_DATA protocolSpecificData{};
PSTORAGE_PROTOCOL_DATA_DESCRIPTOR protocolDataDescriptor{};
};
Definition side:
A function I use to retro-interop error messages to the managed side; it is critical for the understanding of what goes wrong, how/where:
template<class ...T>
auto NVmeQuery::LogMessage(T&&... args) -> void
{
wchar_t updatedMessage[256];
swprintf_s(updatedMessage, forward<T>(args)...);
if (LogMessageChangedCallback != nullptr)
LogMessageChangedCallback(updatedMessage);
}
The core function, which was not straightforward to design. It has 4 parts:
1. getting a handle to the NVMe drive
2. preparing the query
3. do the query
4. check and transmit results
auto NVmeQuery::GetTemp(const wchar_t* nvmePath) -> unsigned long
{
auto nvmeHandle = CreateFile(nvmePath, 0, 0,
0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
{
auto lastErrorID = GetLastError();
if (lastErrorID != 0)
{
LPVOID errorBuffer{};
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, lastErrorID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&errorBuffer, 0, nullptr);
LogMessage(L"Query: ERROR creating handle to NVMe [%s]: %d, %s", nvmePath, lastErrorID, errorBuffer);
}
}
unsigned long bufferLength = FIELD_OFFSET(STORAGE_PROPERTY_QUERY, AdditionalParameters)
+ sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA) + NVME_MAX_LOG_SIZE;
void *buffer = malloc(bufferLength);
ZeroMemory(buffer, bufferLength);
query = (PSTORAGE_PROPERTY_QUERY)buffer;
protocolDataDescriptor = (PSTORAGE_PROTOCOL_DATA_DESCRIPTOR)buffer;
protocolSpecificData = (PSTORAGE_PROTOCOL_SPECIFIC_DATA)query->AdditionalParameters;
query->PropertyId = StorageDeviceProtocolSpecificProperty;
query->QueryType = PropertyStandardQuery;
protocolSpecificData->ProtocolType = ProtocolTypeNvme;
protocolSpecificData->DataType = NVMeDataTypeLogPage;
protocolSpecificData->ProtocolDataRequestValue = NVME_LOG_PAGE_HEALTH_INFO;
protocolSpecificData->ProtocolDataRequestSubValue = 0;
protocolSpecificData->ProtocolDataOffset = sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA);
protocolSpecificData->ProtocolDataLength = sizeof(NVME_HEALTH_INFO_LOG);
unsigned long returnedLength{};
continuing, the actual query and then the miscellaneous checkings:
auto result = DeviceIoControl(nvmeHandle, IOCTL_STORAGE_QUERY_PROPERTY,
buffer, bufferLength,
buffer, bufferLength,
&returnedLength, nullptr);
if (!result || returnedLength == 0)
{
auto lastErrorID = GetLastError();
LPVOID errorBuffer{};
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, lastErrorID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&errorBuffer, 0, nullptr);
LogMessage(L"Query: drive path: %s, nvmeHandle %lu", nvmePath, nvmeHandle);
LogMessage(L"Query: ERROR DeviceIoControl 0x%x %s", lastErrorID, errorBuffer);
}
if (protocolDataDescriptor->Version != sizeof(STORAGE_PROTOCOL_DATA_DESCRIPTOR) ||
protocolDataDescriptor->Size != sizeof(STORAGE_PROTOCOL_DATA_DESCRIPTOR))
{
LogMessage(L"Query: Data descriptor header not valid (size of descriptor: %llu)", sizeof(STORAGE_PROTOCOL_DATA_DESCRIPTOR));
LogMessage(L"Query: DataDesc: version %lu, size %lu", protocolDataDescriptor->Version, protocolDataDescriptor->Size);
}
protocolSpecificData = &protocolDataDescriptor->ProtocolSpecificData;
if (protocolSpecificData->ProtocolDataOffset < sizeof(STORAGE_PROTOCOL_SPECIFIC_DATA) ||
protocolSpecificData->ProtocolDataLength < sizeof(NVME_HEALTH_INFO_LOG))
LogMessage(L"Query: ProtocolData Offset/Length not valid");
SmartHealthInfo = (PNVME_HEALTH_INFO_LOG)((PCHAR)protocolSpecificData + protocolSpecificData->ProtocolDataOffset);
CloseHandle(nvmeHandle);
auto temp = ((ULONG)SmartHealthInfo->Temperature[1] << 8 | SmartHealthInfo->Temperature[0]) - 273;
return temp;
} // end of GetTemp
And for the interop:
Pinvoke auto __stdcall New(MessageChangedCallback managedDelegate) -> NVmeQuery*
{
return new NVmeQuery(managedDelegate);
}
Pinvoke auto GetTemp(NVmeQuery* p, const wchar_t* nvmePath) -> unsigned long
{
return p->GetTemp(nvmePath);
}
And c# side:
public static class NVMeQuery
{
[DllImport("NVMeQuery.dll", CallingConvention = CallingConvention.StdCall)]
internal static extern IntPtr New(InteropBase.AssetCallback callback);
[DllImport("NVMeQuery.dll", CallingConvention = CallingConvention.Cdecl)]
internal static extern ulong GetTemp(IntPtr p, IntPtr drivePath);
}
public class NVMeQueries : InteropBase
{
public NVMeQueries()
{
_ptr = NVMeQuery.New(_onAssetErrorMessageChanged);
}
public ulong GetTemp() => GetTemp(#"\\.\PhysicalDrive4");
public ulong GetTemp(string drivePath)
{
var strPtr = Marshal.StringToHGlobalAuto(drivePath);
var result = NVMeQuery.GetTemp(_ptr, strPtr);
Marshal.FreeHGlobal(strPtr);
return result;
}
}
And the generic base class I use for interop:
public class InteropBase : INotifyPropertyChanged
{
protected IntPtr _ptr;
protected readonly AssetCallback _onAssetErrorMessageChanged;
public delegate void AssetCallback(IntPtr strPtr);
public List<string> LogMessages { get; private set; } = new List<string>();
public InteropBase()
{
_onAssetErrorMessageChanged = LogUpdater;
}
private unsafe void LogUpdater(IntPtr strPtr)
{
var LastLogMessage = new string((char*)strPtr);
LogMessages.Add(LastLogMessage);
OnPropertyChanged(nameof(LogMessages));
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
In my case, the NVMe drive I wanted to query was the 4th physical drive. We get all of them with:
Get-WmiObject Win32_DiskDrive
which will give in my case:
Partitions : 4
DeviceID : \\.\PHYSICALDRIVE4
Model : Samsung SSD 970 EVO Plus 1TB
Size : 1000202273280
Caption : Samsung SSD 970 EVO Plus 1TB
Remarks
This implementation is extremely fast (less that 1ms when there is no LogMessage call); you can define and fill your own structure to get other pieces of information that would be relevant; in that case, the structure must be hold in a field in the native class (eg., SmartHealthInfo in this example) and the query would transmit just the pointer to this structure.
I have implemented the Microsoft Code for this which I think is shorter and simpler - although the code shown here is very elegant. I have built it into a C++ DLL file which can be called by passing an integer value for the Physical Drive number. For my 5 drives, of which 2 are NVMe and 3 SATA, it returns the valid temperatures for the NVMe Drives and zero for the SATA. All take 4 ms. I use a WMI MSStorageDriver_ATAPISmartData call for the SATA drives...quite slow in comparison.
I can call this function from either C++ or VB. The VB implementation still only takes 4ms.
If anyone is after the code or a copy of the DLL file and client implementation then respond and I will list the code here. Otherwise I will assume no further interest.
I am using Visual Studio 2017. I added two projects in a solution. One project is in C# with WPF. The other is in VC++ with ATL.
From C#, I call a function in the VC++ project, which sets low level mouse hook. A part of the code in the low level mouse proc is as follows:
MSLLHOOKSTRUCT stMouse = *(MSLLHOOKSTRUCT*)(lParam);
POINT pt = stMouse.pt;
IAccessible* pAcc;
VARIANT varChild;
HRESULT hr = AccessibleObjectFromPoint(pt, &pAcc, &varChild);
VARIANT varRole;
hr = pAcc->get_accRole(varChild, &varRole);
When I test by clicking on a check box under View tab in MS Word 2013, then I get the role as client for WM_LBUTTONDOWN and WM_LBUTTONUP messages. But I should get the role as check box.
I checked with Inspect.exe that comes with Windows 10 SDK. Inspect.exe shows role correctly as check box. Inspect.exe has two options - one to see UI Automation properties and the other to see MSAA properties. I am seeing the MSAA properties.
How can I get the correct role? What method deos Inspect.exe uses?
Microsoft Active Accessibility / MSAA (based on the IAccessible interface) is a legacy API. You should now use Windows Automation. UIA is based on COM, and uses interfaces, the most important one being IUIAutomationElement. Inspect.exe uses UIA or MSAA.
Note .NET is compatible with UIA and WPF can expose its UI Elements to UIA pretty easily, through the AutomationPeer class and UIElement.OnCreateAutomationPeer Method method. WPF provides a default implementation that can be tweaked if needed.
Here is a similar example to yours in C++, using the UIA API, instead of MSAA (we could write the same using C#):
#include "stdafx.h" // needs <uiautomation.h>
class CCoInitialize { // https://blogs.msdn.microsoft.com/oldnewthing/20040520-00/?p=39243
public:
CCoInitialize() : m_hr(CoInitialize(NULL)) { }
~CCoInitialize() { if (SUCCEEDED(m_hr)) CoUninitialize(); }
operator HRESULT() const { return m_hr; }
HRESULT m_hr;
};
// this is a poor-man COM object class
class CHandler : public IUIAutomationEventHandler
{
public:
HRESULT QueryInterface(REFIID riid, LPVOID * ppv)
{
if (!ppv) return E_INVALIDARG;
*ppv = NULL;
if (riid == IID_IUnknown || riid == IID_IUIAutomationEventHandler)
{
*ppv = this;
return NOERROR;
}
return E_NOINTERFACE;
}
ULONG AddRef() { return 1; }
ULONG Release() { return 1; }
// this will be called by UIA
HRESULT HandleAutomationEvent(IUIAutomationElement *sender, EVENTID eventId)
{
wprintf(L"Event id: %u\n", eventId);
return S_OK;
}
};
int main()
{
CCoInitialize init;
// this sample uses Visual Studio's ATL smart pointers, but it's not mandatory at all
CComPtr<IUIAutomation> uia;
if (SUCCEEDED(uia.CoCreateInstance(CLSID_CUIAutomation)))
{
// get mouse pos now
POINT pt;
GetCursorPos(&pt);
// find what type of "control" was under the mouse
CComPtr<IUIAutomationElement> element;
if (SUCCEEDED(uia->ElementFromPoint(pt, &element)))
{
CComBSTR type;
element->get_CurrentLocalizedControlType(&type);
wprintf(L"type at %u,%u: %s\n", pt.x, pt.y, type.m_str);
}
// get root
CComPtr<IUIAutomationElement> root;
if (SUCCEEDED(uia->GetRootElement(&root)))
{
// add a handler that will trigger every time you open any window on the desktop
// in the real world, you'll need to delete this pointer to avoid memory leaks of course
CHandler *pHandler = new CHandler();
if (SUCCEEDED(uia->AddAutomationEventHandler(UIA_Window_WindowOpenedEventId, root, TreeScope::TreeScope_Children, NULL, pHandler)))
{
// since this is a console app, we need to run the Windows message loop otherwise, you'll never get any UIA events
// for this sample, just press CTRL-C to stop the program. in the real world, you'll need to get out properly
// if you have a standard windows app, the loop will be already there
while (TRUE)
{
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
}
}
return 0;
}
We have several devices where I work (mostly Datalogic 4420 Falcon), and someone is always leaving one off the base. The battery runs dry, then they bring them back to get setup all over. (There's supposed to be a way to configure a file on the SD card to reload upon such an error, but it doesn't work very well)
When someone saves changes on the device (using my app that writes data to the SQL Server), the Serial Number is sent along with it so we can track what devices are in use where.
Each device has a Serial Number, and I have to physically (i.e. manually) write that into the Device name field, which I can read. Working code here if anyone wants to know how:
static string deviceId = null;
public static string DeviceName {
get {
if (String.IsNullOrEmpty(deviceId)) {
using (RegistryKey key = Registry.LocalMachine.OpenSubKey("Ident", true)) {
try {
deviceId = key.GetValue("Name", "[Unnamed]").ToString();
} catch (Exception e) {
ErrorWrapper("GetDeviceName", e);
deviceId = Dns.GetHostName();
} finally {
key.Flush();
key.Close();
}
}
}
return deviceId;
}
}
I do not like the manual (i.e. Fat Finger prone) Serial Number entry. Is there some call to query the device's Serial Number, or is that vendor specific?
Datamax does make an SDK that is specific to their devices, but we don't want our applications tied down to any one manufacturer (we are already tied down to VS2008).
I'd start by trying to P/Invoke to get the device ID (KerneIoControl with IOCTL_HAL_GET_DEVICEID) and see if it matches the serial number you're after. Here's an example.
I don't know about your Datalogic 4420 Falcon device, but I work with Intermec CK30 & CK60 and I have their itc50.dll file.
Here is snippet:
[DllImport("itc50.dll")]public static extern int ITCGetSerialNumber(StringBuilder Snumber, int buffSize);
StringBuilder hwSN = new StringBuilder(12);
if (ITCGetSerialNumber(hwSN, hwSN.Capacity) >= 0)
{
;
;
}
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 5 years ago.
Improve this question
I have been tasked to overwrite all the free space on a few laptops 3 times. I know there are some alternatives but I like to know how things work and if I can to do it myself with C#.
1) yes, I know there are plenty of freeware applications that will do this
2) no, we don't need to conform to any specific government standard
Where do I look for ideas on how to start this?
Thanks if you can point me in the right direction.
Can it be achieved using C#? If so, how?
Simple algorithm:
Create a large text file full of arbitrary text (best to use a pre-created file instead of regenerating random for performance reasons. Test it out.)
Create a clever folder and file naming scheme so as to be able to track the files. You should also track the files with your app but if it crashes, especially near the end of a first few test runs, you'll want to be able to easily find and clean up your handy work.
Write it to the HDD until it's full
Delete the files you created
Repeat above steps two more times
Update: More advanced consideration on wiping per subsequent discussion:
On first pass write files of 0x0000 values (all bits off)
On second pass write all bits as 0xFFFF (all bits on)
On last pass repeat with 0x0000
The above ignores a few ideas such as what is the best size of the file, depends on your file system anyway. You might also get different behaviors from the OS as you near a filled HDD...
This is really dangerous but..
You can use the Defrag APIs (here's a C# wrapper) to get hold of the drive 'map' and specifically target the freespace and write junk to those parts of the disk.
Check the SDelete documentation, maybe you can get a clue there.
You're going to have to do some low level manipulations, so you'll certainly have to talk to the Win32 API. I haven't done this sort of thing, so I can't give you specifics, but a good place to start looking might be the Win32 API reference:http://msdn.microsoft.com/en-us/library/aa383749%28VS.85%29.aspx
I'm really not an expert in this field at all, but it seems to my naive understanding that you'll need to:
1) get info on where the filesystem starts & stops
2) using the non-deleted files as a reference, get a list of physical locations of what should be free space
3) write 0's to those locations
Maybe this isn't a great answer since I'm not an expert in the field, but it was a bit too long for a comment ;) I hope that helps a little.
System.Diagonstics.Process.Start("chipher.exe /WC:\");
This is asynchronous by default, you get the idea.
This code is from The Code Project I think. I'm unsure where the orignal article is, but it does what you asked for:
Based on comments I clearly need to spoonfeed a bit more..
You can do this very simply, based on your requirements.
Make 1 large file that fills the remaining free size on your drive. Then simply wipe this file.
Make several files until your drive is full. (This might be better if you want to use the machine while its going on ). Then you can start wiping each file, so in effect the total time the system has a full hard disj drive is smaller than using method 1. But it will likely be a bit slower and use a bit more code.
The advantage of using are a few, easy code for you to use. You don't have to play with low level APIs that will screw you over.
using System;
using System.IO;
using System.Security.Cryptography;
namespace QuickStarterShared
{
public class Wipe
{
/// <summary>
/// Deletes a file in a secure way by overwriting it with
/// random garbage data n times.
/// </summary>
/// <param name="filename">Full path of the file to be deleted</param>
/// <param name="timesToWrite">Specifies the number of times the file should be overwritten</param>
public void WipeFile(string filename, int timesToWrite)
{
#if !DEBUG
try
{
#endif
if (File.Exists(filename))
{
// Set the files attributes to normal in case it's read-only.
File.SetAttributes(filename, FileAttributes.Normal);
// Calculate the total number of sectors in the file.
double sectors = Math.Ceiling(new FileInfo(filename).Length/512.0);
// Create a dummy-buffer the size of a sector.
byte[] dummyBuffer = new byte[512];
// Create a cryptographic Random Number Generator.
// This is what I use to create the garbage data.
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
// Open a FileStream to the file.
FileStream inputStream = new FileStream(filename, FileMode.Open);
for (int currentPass = 0; currentPass < timesToWrite; currentPass++)
{
// Go to the beginning of the stream
inputStream.Position = 0;
// Loop all sectors
for (int sectorsWritten = 0; sectorsWritten < sectors; sectorsWritten++)
{
// Fill the dummy-buffer with random data
rng.GetBytes(dummyBuffer);
// Write it to the stream
inputStream.Write(dummyBuffer, 0, dummyBuffer.Length);
}
}
// Truncate the file to 0 bytes.
// This will hide the original file-length if you try to recover the file.
inputStream.SetLength(0);
// Close the stream.
inputStream.Close();
// As an extra precaution I change the dates of the file so the
// original dates are hidden if you try to recover the file.
DateTime dt = new DateTime(2037, 1, 1, 0, 0, 0);
File.SetCreationTime(filename, dt);
File.SetLastAccessTime(filename, dt);
File.SetLastWriteTime(filename, dt);
File.SetCreationTimeUtc(filename, dt);
File.SetLastAccessTimeUtc(filename, dt);
File.SetLastWriteTimeUtc(filename, dt);
// Finally, delete the file
File.Delete(filename);
}
#if !DEBUG
}
catch(Exception e)
{
}
#endif
}
}
# region Events
# region PassInfo
public delegate void PassInfoEventHandler(PassInfoEventArgs e);
public class PassInfoEventArgs : EventArgs
{
private readonly int cPass;
private readonly int tPass;
public PassInfoEventArgs(int currentPass, int totalPasses)
{
cPass = currentPass;
tPass = totalPasses;
}
/// <summary> Get the current pass </summary>
public int CurrentPass { get { return cPass; } }
/// <summary> Get the total number of passes to be run </summary>
public int TotalPasses { get { return tPass; } }
}
# endregion
# region SectorInfo
public delegate void SectorInfoEventHandler(SectorInfoEventArgs e);
public class SectorInfoEventArgs : EventArgs
{
private readonly int cSector;
private readonly int tSectors;
public SectorInfoEventArgs(int currentSector, int totalSectors)
{
cSector = currentSector;
tSectors = totalSectors;
}
/// <summary> Get the current sector </summary>
public int CurrentSector { get { return cSector; } }
/// <summary> Get the total number of sectors to be run </summary>
public int TotalSectors { get { return tSectors; } }
}
# endregion
# region WipeDone
public delegate void WipeDoneEventHandler(WipeDoneEventArgs e);
public class WipeDoneEventArgs : EventArgs
{
}
# endregion
# region WipeError
public delegate void WipeErrorEventHandler(WipeErrorEventArgs e);
public class WipeErrorEventArgs : EventArgs
{
private readonly Exception e;
public WipeErrorEventArgs(Exception error)
{
e = error;
}
public Exception WipeError{get{ return e;}}
}
# endregion
# endregion
}
How can a Windows console application written in C# determine whether it is invoked in a non-interactive environment (e.g. from a service or as a scheduled task) or from an environment capable of user-interaction (e.g. Command Prompt or PowerShell)?
[EDIT: 4/2021 - new answer...]
Due to a recent change in the Visual Studio debugger, my original answer stopped working correctly when debugging. To remedy this, I'm providing an entirely different approach. The text of the original answer is included at the bottom.
1. Just the code, please...
To determine if a .NET application is running in GUI mode:
[DllImport("kernel32.dll")] static extern IntPtr GetModuleHandleW(IntPtr _);
public static bool IsGui
{
get
{
var p = GetModuleHandleW(default);
return Marshal.ReadInt16(p, Marshal.ReadInt32(p, 0x3C) + 0x5C) == 2;
}
}
This checks the Subsystem value in the PE header. For a console application, the value will be 3 instead of 2.
2. Discussion
As noted in a related question, the most reliable indicator of GUI vs. console is the "Subsystem" field in the PE header of the executable image. The following C# enum lists the allowable (documented) values:
public enum Subsystem : ushort
{
Unknown /**/ = 0x0000,
Native /**/ = 0x0001,
WindowsGui /**/ = 0x0002,
WindowsCui /**/ = 0x0003,
OS2Cui /**/ = 0x0005,
PosixCui /**/ = 0x0007,
NativeWindows /**/ = 0x0008,
WindowsCEGui /**/ = 0x0009,
EfiApplication /**/ = 0x000A,
EfiBootServiceDriver /**/ = 0x000B,
EfiRuntimeDriver /**/ = 0x000C,
EfiRom /**/ = 0x000D,
Xbox /**/ = 0x000E,
WindowsBootApplication /**/ = 0x0010,
};
As easy as that code (in that other answer) is, our case here can be vastly simplified. Since we are only specifically interested in our own running process (which is necessarily loaded), you don't have to open any file or read from the disk to obtain the subsystem value. Our executable image is guaranteed to be already mapped into memory. And it is simple to retrieve the base address for any loaded file image by calling the GetModuleHandleW function:
[DllImport("kernel32.dll")]
static extern IntPtr GetModuleHandleW(IntPtr lpModuleName);
Although we might provide a filename to this function, again things are easier and we don't have to. Passing null, or in this case, default(IntPtr.Zero) (which is the same as IntPtr.Zero), returns the base address of the virtual memory image for the current process. This eliminates the extra steps (alluded to earlier) of having to fetch the entry assembly and its Location property, etc. Without further ado, here is the new and simplified code:
static Subsystem GetSubsystem()
{
var p = GetModuleHandleW(default); // VM base address of mapped PE image
p += Marshal.ReadInt32(p, 0x3C); // RVA of COFF/PE within DOS header
return (Subsystem)Marshal.ReadInt16(p + 0x5C); // PE offset to 'Subsystem' word
}
public static bool IsGui => GetSubsystem() == Subsystem.WindowsGui;
public static bool IsConsole => GetSubsystem() == Subsystem.WindowsCui;
[official end of the new answer]
3. Bonus Discussion
For the purposes of .NET, Subsystem is perhaps the most—or only—useful piece of information in the PE Header. But depending on your tolerance for minutiae, there could be other invaluable tidbits, and it's easy to use the technique just described to retrieve additional interesting data.
Obviously, by changing the final field offset (0x5C) used earlier, you can access other fields in the COFF or PE header. The next snippet illustrates this for Subsystem (as above) plus three additional fields with their respective offsets.
NOTE: To reduce clutter, the enum declarations used in the following can be found here
var p = GetModuleHandleW(default); // PE image VM mapped base address
p += Marshal.ReadInt32(p, 0x3C); // RVA of COFF/PE within DOS header
var subsys = (Subsystem)Marshal.ReadInt16(p + 0x005C); // (same as before)
var machine = (ImageFileMachine)Marshal.ReadInt16(p + 0x0004); // new
var imgType = (ImageFileCharacteristics)Marshal.ReadInt16(p + 0x0016); // new
var dllFlags = (DllCharacteristics)Marshal.ReadInt16(p + 0x005E); // new
// ... etc.
To improve things when accessing multiple fields in unmanaged memory, it's essential to define an overlaying struct. This allows for direct and natural managed access using C#. For the running example, I merged the adjacent COFF and PE headers together into the following C# struct definition, and only included the four fields we deemed interesting:
[StructLayout(LayoutKind.Explicit)]
struct COFF_PE
{
[FieldOffset(0x04)] public ImageFileMachine MachineType;
[FieldOffset(0x16)] public ImageFileCharacteristics Characteristics;
[FieldOffset(0x5C)] public Subsystem Subsystem;
[FieldOffset(0x5E)] public DllCharacteristics DllCharacteristics;
};
NOTE: A fuller version of this struct, without the omitted fields, can be found here
Any interop struct such as this has to be properly setup at runtime, and there are many options for doing so. Ideally, its generally better to impose the struct overlay "in-situ" directly on the unmanaged memory, so that no memory copying needs to occur. To avoid prolonging the discussion here even further however, I will instead show an easier method that does involve copying.
var p = GetModuleHandleW(default);
var _pe = Marshal.PtrToStructure<COFF_PE>(p + Marshal.ReadInt32(p, 0x3C));
Trace.WriteLine($#"
MachineType: {_pe.MachineType}
Characteristics: {_pe.Characteristics}
Subsystem: {_pe.Subsystem}
DllCharacteristics: {_pe.DllCharacteristics}");
4. Output of the demo code
Here is the output when a console program is running...
MachineType: Amd64
Characteristics: ExecutableImage, LargeAddressAware
Subsystem: WindowsCui (3)
DllCharacteristics: HighEntropyVA, DynamicBase, NxCompatible, NoSeh, TSAware
...compared to GUI (WPF) application:
MachineType: Amd64
Characteristics: ExecutableImage, LargeAddressAware
Subsystem: WindowsGui (2)
DllCharacteristics: HighEntropyVA, DynamicBase, NxCompatible, NoSeh, TSAware
[OLD: original answer from 2012...]
To determine if a .NET application is running in GUI mode:
bool is_console_app = Console.OpenStandardInput(1) != Stream.Null;
Environment.UserInteractive Property
If all you're trying to do is to determine whether the console will continue to exist after your program exits (so that you can, for example, prompt the user to hit Enter before the program exits), then all you have to do is to check if your process is the only one attached to the console. If it is, then the console will be destroyed when your process exits. If there are other processes attached to the console, then the console will continue to exist (because your program won't be the last one).
For example*:
using System;
using System.Runtime.InteropServices;
namespace CheckIfConsoleWillBeDestroyedAtTheEnd
{
internal class Program
{
private static void Main(string[] args)
{
// ...
if (ConsoleWillBeDestroyedAtTheEnd())
{
Console.WriteLine("Press any key to continue . . .");
Console.ReadKey();
}
}
private static bool ConsoleWillBeDestroyedAtTheEnd()
{
var processList = new uint[1];
var processCount = GetConsoleProcessList(processList, 1);
return processCount == 1;
}
[DllImport("kernel32.dll", SetLastError = true)]
static extern uint GetConsoleProcessList(uint[] processList, uint processCount);
}
}
(*) Adapted from code found here.
I haven't tested it, but Environment.UserInteractive looks promising.
A possible improvement of Glenn Slayden's solution:
bool isConsoleApplication = Console.In != StreamReader.Null;
To prompt for user input in an interactive console, but do nothing when run without a console or when input has been redirected:
if (Environment.UserInteractive && !Console.IsInputRedirected)
{
Console.ReadKey();
}