.NET System::Diagnostics::Process which uses a DLL C++ - c++

My current code looks something like this:
String^ callProcess(String^ cmd, String^ args)
{
Process^ p = gcnew Process();
p->StartInfo->UseShellExecute = false;
p->StartInfo->RedirectStandardOutput = true;
p->StartInfo->CreateNoWindow = true;
p->StartInfo->Arguments = args;
p->StartInfo->FileName = cmd;
p->Start();
p->WaitForExit();
String^ output = p->StandardOutput->ReadToEnd();
p->Close();
return output;
}
int main()
{
String^ output1 = callProcess("handle.exe","-arg1");
String^ output2 = callProcess("handle.exe","-arg2");
}
handle.exe opens a handle to a DLL let's call it HANDLE.DLL. After trying to look at output2 it has something like "Error during SomeClass Call.... Can't get a handle to HANDLE.DLL".
It looks like the first call to CallProcess isn't properly releasing the handle to the DLL. Is there a way I can force it to release the DLL handle?

Related

Passing std::string to C++ DLL for use with C# dll

I am trying to pass strings from a C++ console app to a C++ dll, which i then convert to System::string to be sent to a C# dll. But the string comes through as random characters. Note that lShutdown actually comes through correct as it is defined in the dll so the problem must be between the testapp and the C++ dll. I had this working before, but i cant figure out for the life of me how i did it
Here is the code for passing to the dll:
void GMSTestAppC::GMSSetup()
{
string processName = "GMSCTestApp";
string progName = "TestApp";
//Directory where program is installed
string progDir = "C:/";
string version = "4.0.0.1";
//Directory for config file
string configDir = "C:/";
//Directory for log files
string logDir = "C:/";
string user = "Smurf";
string shutdownby = "";
Initialize(processName, GetCurrentProcessId(), progName, progDir, version, configDir, logDir, user, shutdownby);
}
Here is the dll code:
bool Initialize(std::string& processName, int processID, std::string& programName, std::string& programDir, std::string& version, std::string& configDir, std::string& logDir, std::string& currentUser, std::string& lastShutDownBy)
{
String^ pName = gcnew String(processName.c_str());
String^ progName = gcnew String(programName.c_str());
String^ progDir = gcnew String(programDir.c_str());
String^ cDir = gcnew String(configDir.c_str());
String^ lDir = gcnew String(logDir.c_str());
String^ cUser = gcnew String(currentUser.c_str());
String^ lShutdown = gcnew String("test");
GMS::GMSVersion fv;
sscanf_s(version.c_str(), "%d.%d.%d.%d", &fv.Major, &fv.Minor, &fv.Revision, &fv.Build);
int pID = int(processID);
Return GMS::Initialize(pName, pID, progName, progDir, fv, cDir, lDir, cUser, lShutdown);
}
Here is what i actually get through:
Sent: <InitializeProgram>
<TimeStamp>2018-01-22T14:34:37.6152709+00:00</TimeStamp>
<PID>13504</PID>
<ProcessName>(÷'</ProcessName>
<ProgramName>Ðð'</ProgramName>
<ProgramDir>°ñ'</ProgramDir>
<Version>0.0.0.0</Version>
<ConfigDir>^ô'</ConfigDir>
<LogDir>èñ'</LogDir>
<CurrentUser>÷'</CurrentUser>
<LastShutDownBy>test</LastShutDownBy>
</InitializeProgram>
The characters that come through are different each time the program is run
Any help would be greatly appreciated
If you are using C++11 (which you most probably be using), you can try using std::u32string. It removes the need to use std::wstring and is standard across platforms.

Dynamic loading Leadtools DLLs

I am using Leadtools 17.5. If I statically link the Leadtools Dlls into my 64 bit C++ Application and then call L_SetLicenseBuffer everything works fine and the return value is zero. But for security reasons, the final product is not allowed to add those DLLs into the System32 folder and is also not allowed to change the system path, and since multiple applications are using the tools I want to install them in a common folder (C:\Program Files\Common Files\LeadTools\17.5 for example) and use AddDllDirectory to add the path to the DLL search path. So I decided to load the DLLs dynamically at the run-time. So I created a definition for the function like this:
typedef L_INT (EXT_FUNCTION* TL_SetLicenseBuffer)(L_UCHAR* pLicenseBuffer, L_SSIZE_T nSize, L_TCHAR* pszDeveloperKey);
typedef L_BOOL (EXT_FUNCTION* TL_IsSupportLocked)(L_UINT uType);
then created a function pointer like this:
TL_SetLicenseBuffer pfSetLicenseBuffer = NULL;
TL_IsSupportLocked pfIsSupportLocked = NULL;
then add the paths to where the DLLs are to the DLL search path:
AddDllDirectory(LEAD_DLL_PATH);
AddDllDirectory(LEAD_FILTER_PATH);
and set the default directory search path for DLLs to be the user defined:
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_USER_DIRS);
then load the DLL and get the address of the functions I need:
HINSTANCE hKrn = LoadLibrary(L"ltkrnx.dll");
pfSetLicenseBuffer = (TL_SetLicenseBuffer)GetProcAddress(hKrn, "L_SetLicenseBuffer");
pfIsSupportLocked = (TL_IsSupportLocked)GetProcAddress(hKrn, "L_IsSupportLocked");
now if I use the function pointer with the same parameters as before, the function fails and returns -13 and any subsequent call to for example to pfIsSupportLocked shows the nag dialog:
retCode = pfSetLicenseBuffer(pLicenseData, LicSize, pKeyStr); // retCode is -13
pfIsSupportLocked(L_SUPPORT_DOCUMENT); // Shows nag dialog
Does anyone know how I can fix this?
Thank you
Sam
The first thing you need to do is check the debugger output and make sure that the DLL you are expecting to get loaded is the one getting loaded by verifying the path. It is possible that you have multiple versions of LTKRNX.DLL in your search path. I have tested your code here and it returned SUCCESS:
typedef L_INT (EXT_FUNCTION* TL_SetLicenseBuffer)(L_UCHAR* pLicenseBuffer, L_SSIZE_T nSize, L_TCHAR* pszDeveloperKey);
typedef L_BOOL (EXT_FUNCTION* TL_IsSupportLocked)(L_UINT uType);
HINSTANCE hKrn = LoadLibrary(L"ltkrnx.dll");
TL_SetLicenseBuffer pfSetLicenseBuffer = NULL;
TL_IsSupportLocked pfIsSupportLocked = NULL;
pfSetLicenseBuffer = (TL_SetLicenseBuffer)GetProcAddress(hKrn, "L_SetLicenseBuffer");
pfIsSupportLocked = (TL_IsSupportLocked)GetProcAddress(hKrn, "L_IsSupportLocked");
L_INT retCode = pfSetLicenseBuffer(szLICAnsi, _countof(szLICAnsi), pKeyStr);
if(retCode == SUCCESS)
bRet = pfIsSupportLocked(L_SUPPORT_DOCUMENT);
else
printf("Problem!");
Also what PaulMcKenzie suggested is another way to verify that your calls to LoadLibrary are working correctly. If you still cannot figure it out, you can contact our Technical Support to assist you with this issue at support#leadtools.com
I was not able to make the dynamic loading to work at all, but I was able to use Delay loading to work.What I had to do was to go back to linking the extracted .Lib files to my application and then tell compiler to load the associated DLLs with delay, which gave me a chance to create Notification Hooks to __pfnDliNotifyHook2 and __pfnDliFailureHook2 and that way I could use LoadLibrary to load the delayed loaded Dlls from correct location.But that only fixed half the problem because some of these Dlls are dependent on other DLLs and when I used the full path to load the DLL that I wanted, it could not find the secondary DLLs (which were located in the same directory as the one I was loading) and that would cause LoadLibrary to fail. The solution was to keep track of those dependencies and pre-load them. I am including some of the code to fix the issue for anyone whom might run into similar situation later on.P. S. I am using Embarcadero's C++ Builder, so Some of the objects like the Strings, TStringList and Exception may not be exactly what everyone is familiar with, but the concept should work in VC++ as well.
#include <map>
struct TDllDependency
{
TStringList* Dependency;
HMODULE hDll;
__fastcall TDllDependency(void)
{
hDll = NULL;
Dependency = new TStringList();
}
virtual __fastcall ~TDllDependency(void)
{
delete Dependency;
}
};
class TDllModList : public std::map<System::String, TDllDependency>
{
public:
void __fastcall CheckDependency(const System::String& aName);
};
//---------------------------------------------------------------------------
System::String __fastcall GetLtDllPath(void)
{
wchar_t* pfPath = NULL;
System::String dllPath;
SHGetKnownFolderPath(FOLDERID_ProgramFilesCommon, KF_FLAG_DEFAULT, NULL, &pfPath);
if (NULL != pfPath)
{
dllPath = IncludeTrailingBackslash(pfPath) + L"LeadTools\\17.5\\";
::CoTaskMemFree(pfPath);
}
return dllPath;
}
System::String mDllPath(GetLtDllPath());
TDllModList DllModList;
void __fastcall InitDllDepends()
{
DllModList.clear();
#if defined(_WIN64)
DllModList[L"ltimgefxx.dll"].Dependency->CommaText = L"ltdisx.dll,ltimgutlx.dll";
DllModList[L"ltefxx.dll"].Dependency->CommaText = L"ltdisx.dll,ltimgutlx.dll";
DllModList[L"ltimgcorx.dll"].Dependency->CommaText = L"ltdisx.dll,ltimgutlx.dll";
DllModList[L"ltdlgimgefxx.dll"].Dependency->CommaText = L"ltdisx.dll,ltdlgkrnx.dll,ltdlgcomx.dll,ltdlgctrlx.dll,ltdlgutlx.dll,ltimgefxx.dll,ltimgsfxx.dll,ltimgcorx.dll,ltimgclrx.dll";
DllModList[L"ltdlgutlx.dll"].Dependency->CommaText = L"ltdisx.dll,ltfilx.dll,ltdlgkrnx.dll,ltimgclrx.dll,ltimgcorx.dll,ltimgefxx.dll,ltimgsfxx.dll";
DllModList[L"ltdlgctrlx.dll"].Dependency->CommaText = L"ltdlgutlx.dll,ltdlgkrnx.dll,ltdisx.dll,ltfilx.dll,ltimgefxx.dll";
DllModList[L"ltdlgcomx.dll"].Dependency->CommaText = L"ltdlgkrnx.dll,ltdlgctrlx.dll,ltdlgutlx.dll";
#elif defined(__WIN32__)
DllModList[L"ltimgefxu.dll"].Dependency->CommaText = L"ltdisu.dll,ltimgutlu.dll";
DllModList[L"ltefxu.dll"].Dependency->CommaText = L"ltdisu.dll,ltimgutlu.dll";
DllModList[L"ltimgcoru.dll"].Dependency->CommaText = L"ltdisu.dll,ltimgutlu.dll";
DllModList[L"ltdlgimgefxu.dll"].Dependency->CommaText = L"ltdisu.dll,ltdlgkrnu.dll,ltdlgcomu.dll,ltdlgctrlu.dll,ltdlgutlu.dll,ltimgefxu.dll,ltimgsfxu.dll,ltimgcoru.dll,ltimgclru.dll";
DllModList[L"ltdlgutlu.dll"].Dependency->CommaText = L"ltdisu.dll,ltfilu.dll,ltdlgkrnu.dll,ltimgclru.dll,ltimgcoru.dll,ltimgefxu.dll,ltimgsfxu.dll";
DllModList[L"ltdlgctrlu.dll"].Dependency->CommaText = L"ltdlgutlu.dll,ltdlgkrnu.dll,ltdisu.dll,ltfilu.dll,ltimgefxu.dll";
DllModList[L"ltdlgcomu.dll"].Dependency->CommaText = L"ltdlgkrnu.dll,ltdlgctrlu.dll,ltdlgutlu.dll";
#endif
};
HMODULE SafeLoadLeadDll(const System::String tName)
{
System::String tPath;
HMODULE retVal = NULL;
DllModList.CheckDependency(tName);
tPath = mDllPath + tName;
if(FileExists(tPath))
retVal = ::LoadLibrary(tPath.c_str());
return retVal;
}
FARPROC WINAPI MyDliNotifyHook(unsigned dliNotify, PDelayLoadInfo pdli)
{
FARPROC retVal = NULL;
System::String tStr(pdli->szDll);
tStr = tStr.LowerCase();
if(dliNotePreLoadLibrary == dliNotify)
{
TDllModList::iterator i = DllModList.find(tStr);
if(DllModList.end() == i)
{
retVal = (FARPROC)SafeLoadLeadDll(tStr);
DllModList[tStr].hDll = (HMODULE)retVal;
}
else if(NULL == i->second.hDll)
{
i->second.hDll = SafeLoadLeadDll(tStr);
retVal = (FARPROC)i->second.hDll;
}
else
retVal = (FARPROC)i->second.hDll;
}
else if(dliFailLoadLib == dliNotify)
{
tStr = L"Compleatly falied to load " + tStr;
::OutputDebugString(tStr.c_str());
}
return retVal;
}
FARPROC WINAPI MyDliFailureHook(unsigned dliNotify, PDelayLoadInfo pdli)
{
FARPROC retVal = NULL;
if(dliNotePreLoadLibrary == dliNotify)
{
System::String tMsg = pdli->szDll;
tMsg = L"Failed to load \"" + tMsg + L"\".\n" + SysErrorMessage(::GetLastError());
throw Exception(tMsg);
}
return retVal;
}
extern "C" PfnDliHook __pfnDliNotifyHook2 = MyDliNotifyHook;
extern "C" PfnDliHook __pfnDliFailureHook2 = MyDliFailureHook;
void __fastcall TDllModList::CheckDependency(const System::String& aName)
{
TDllModList::iterator i = find(aName);
if(end() != i)
{
int len = i->second.Dependency->Count;
int j;
System::String tPath;
for(j = 0; j < len; j++)
{
if(end() == find(i->second.Dependency->Strings[j]))
{
CheckDependency(i->second.Dependency->Strings[j]);
tPath = mDllPath + i->second.Dependency->Strings[j];
(*this)[i->second.Dependency->Strings[j]].hDll = ::LoadLibrary(tPath.c_str());
}
}
}
}
//---------------------------------------------------------------------------
And of course InitDllDepends(); should be called at the beginning of WinMain to set things up correctly.

C++ CLR unmanaged BYTE* to managed System::array<Byte>^ without loop

I tried to find working solution on my qestion, but not found.
I want to "connect" managed C# .NET code with native C++ code (DLL). My C++ returns unmanaged BYTE* pointer and count of elements.
How to initialize new array<Byte>^ from BYTE* without loop? Because it may be to 5 Megabytes. (Encrypting data)
Help if you can please.
EDIT:
With loop it works:
array<Byte>^ a = gcnew array<Byte>(resultDataSize);
for (int i = 0; i < resultDataSize; i++)
{
a[i] = resultData[i];
}
But I'd like get something like:
a = new array<Byte>(resultData, resultDataSize);
EDIT 2:
Now in my C++ i have function:
void Decrypt(BYTE* dataToDecrypt, DWORD dataToDecryptLength, wchar_t* senderName, wchar_t* receiverName, BYTE** result, DWORD* resultSize)
{
...
*result = main.Data;
*resultSize = main.Length;
}
Where main is a class with BYTE* Data and DWORD Length and other functions.
In my CLR:
array<Byte>^ Decrypt(array<Byte>^ dataToDecrypt, String^ senderName, String^ receiverName)
{
...
BYTE* resultData = nullptr;
DWORD resultDataSize = 0;
// Call my c++
DecryptFunction(dataToDecryptBytes, dataToDecrypt->Length, sender, receiver, &resultData, &resultDataSize);
array<Byte>^ result = gcnew array<Byte>(resultDataSize);
for (int i = 0; i < resultDataSize; i++)
{
result[i] = resultData[i];
}
....
return result;
}
In C#:
byte[] data = ...;
...
byte[] decrypted = provider.Decrypt(data, "user1", "user2");
...

winrt c++/cx concurrency access violation exception

What I'm trying to do is check for the existence of a file in the local folder and then copy it there if it isn't found (the file was previously added to the project as an asset).
Here is the code:
Windows::Storage::StorageFile^ MainPage::GetCustomFileAsync(Platform::String^ fileName)
{
using Windows::Storage::StorageFile;
using Windows::Storage::StorageFolder;
auto localFolder = Windows::Storage::ApplicationData::Current->LocalFolder;
auto localTask = concurrency::create_task(localFolder->GetFileAsync(fileName));
StorageFile^ retVal = nullptr;
localTask.then([&](StorageFile^ t){
retVal = t;
}).then([](concurrency::task<void> t)
{
try
{
t.get();
OutputDebugString(L"Found\n");
}
catch (Platform::COMException^ e)
{
OutputDebugString(e->Message->Data());
}
}).wait();
return retVal;
}
StorageFile^ fileVar;
if ((fileVar = this->GetCustomFileAsync("somefile.txt")) == nullptr)
{
String^ path = Windows::ApplicationModel::Package::Current->InstalledLocation->Path + "\\Assets";
concurrency::create_task(Windows::Storage::StorageFolder::GetFolderFromPathAsync(path)).then([](StorageFolder^ folder){
return (folder->GetFileAsync("somefile.txt"));
}).then([](StorageFile^ file){
return (file->CopyAsync(Windows::Storage::ApplicationData::Current->LocalFolder));
}).then([&](StorageFile^ file){
fileVar = file;
OutputDebugString(file->DisplayName->Data());
});
}
What happens is that I get an access violation exception at the point where "file" is being assigned to "fileVar" (because of cross-thread access perhaps?). How to fix this?
Edit: I can't do all the processing there because the file will be accessed many times. In short I need to know when it has been successfully copied and get a handle to it. Here is the code that works
Windows::Storage::StorageFile^ GetFile(Platform::String^ fileName)
{
using Windows::Storage::StorageFile;
using Windows::Storage::StorageFolder;
using Windows::Foundation::AsyncOperationCompletedHandler;
using Windows::Foundation::AsyncStatus;
using Windows::Foundation::IAsyncOperation;
using Platform::String;
auto localFolder = Windows::Storage::ApplicationData::Current->LocalFolder;
bool completed = false;
StorageFile^ retVal = nullptr;
localFolder->GetFileAsync(fileName)->Completed = ref new AsyncOperationCompletedHandler<StorageFile^>([&completed, &retVal, &fileName](IAsyncOperation<StorageFile^>^ fileOperation, AsyncStatus status)
{
if (status == AsyncStatus::Error)
{
String^ path = Windows::ApplicationModel::Package::Current->InstalledLocation->Path + "\\Assets";
Windows::Storage::StorageFolder::GetFolderFromPathAsync(path)->Completed = ref new AsyncOperationCompletedHandler<Windows::Storage::StorageFolder^>(
[&completed, &retVal, &fileName](IAsyncOperation<Windows::Storage::StorageFolder^>^ folderOperation, AsyncStatus status)->void{
auto assetFolder = folderOperation->GetResults();
assetFolder->GetFileAsync(fileName)->Completed = ref new AsyncOperationCompletedHandler<Windows::Storage::StorageFile^>([&completed, &retVal, &fileName](IAsyncOperation<Windows::Storage::StorageFile^>^ fileOperation, AsyncStatus status)->void{
auto file = fileOperation->GetResults();
file->CopyAsync(Windows::Storage::ApplicationData::Current->LocalFolder)->Completed = ref new AsyncOperationCompletedHandler<Windows::Storage::StorageFile^>
([&completed, &retVal, &fileName](IAsyncOperation<Windows::Storage::StorageFile^>^ fileOperation, AsyncStatus status)->void {
retVal = fileOperation->GetResults();
completed = true;
});
});
});
}
else
{
retVal = fileOperation->GetResults();
completed = true;
}
});
while (completed == false);
return retVal;
}
Rather than passing a delegate as an argument and returning void, make your method return task<StorageFile^> and then the caller can do a .then() to continue working once the operation has succeeded.
Or if this is exposed as a public WinRT method (not an internal / private C++ method) then use IAsyncOperation<StorageFile^>^ as the return type, and wrap the whole thing in create_async():
IAsyncOperation<StorageFile^>^ DoStuff(params)
{
return concurrency::create_async([params]
{
// function body goes here
});
}
Here's a solution I put together. Two things that are important to know:
When executing an asynchronous operation using concurrency::create_task the async operation(s) can still be executing when the parent function returns. So the captured variables MUST outlive the context of the parent function. Which obviously won't happen if they are being passed by reference. It took a while to realize this.
WinRT imposes certain restrictions on the concurrency runtime. Calling concurrency::task::get() or concurrency::task::wait() will throw an exception in an STA thread, unless the call is in a task continuation.
More information in this post:
http://social.msdn.microsoft.com/Forums/windowsapps/en-US/ae54980b-41ce-4337-a059-2213b549be4b/concurrencyinvalidoperation-when-calling-tasktget?forum=winappswithnativecode
In that case how to know when the function has finished doing it's job? I opted to pass in a callback (AKA delegate).
delegate void FileOperation(Windows::Storage::StorageFile^ file);
void GetFileConcurrency(Platform::String^ fileName, FileOperation^ fileOp)
{
using Windows::Storage::StorageFile;
using Windows::Storage::StorageFolder;
using Platform::String;
auto localFolder = Windows::Storage::ApplicationData::Current->LocalFolder;
String^ assetFolderPath = Windows::ApplicationModel::Package::Current->InstalledLocation->Path + "\\Assets";
auto localFolderTask = concurrency::create_task(localFolder->GetFileAsync(fileName));
localFolderTask.then([localFolder, assetFolderPath, fileName, fileOp](concurrency::task<StorageFile^> theTask){
try
{
StorageFile^ theFile = theTask.get();
fileOp(theFile);
}
catch (Platform::Exception^ e)
{
OutputDebugString(e->Message->Data());
auto assetFolderTask = concurrency::create_task(StorageFolder::GetFolderFromPathAsync(assetFolderPath));
assetFolderTask.then([localFolder, assetFolderPath, fileName, fileOp](StorageFolder^ assetFolder){
auto assetFileTask = concurrency::create_task(assetFolder->GetFileAsync(fileName));
assetFileTask.then([localFolder, assetFolderPath, fileName, fileOp](StorageFile^ file){
auto copyFileTask = concurrency::create_task(file->CopyAsync(localFolder));
copyFileTask.then([localFolder, assetFolderPath, fileName, fileOp](StorageFile^ file){
OutputDebugString(file->Path->Data());
fileOp(file);
});
});
});
}
});
}

Delete entire line if there is a word in it

I am creating a Windows form program. In a form I insert some data through textboxes, and I write them in a file. Once I press the register button I register a line and so on.
Example:
test1|test2|test3...
test4|test5|test6...
In another form I type a word in a textbox and if this word is in my file I want to delete that line.
Example: the word is test5, so I will delete all the line where it is. I'll have:
test1|test2|test3...
Thanks to Medinoc user for this:
WRITE
ref class MyClass
{
public:
String^ cognome;
String^ nome;
int voto_diploma;
};
//...
List<MyClass^>^ primo = gcnew List<MyClass^>();
//...
MyClass^ myObj = gcnew MyClass();
myObj->cognome = textBox1->Text;
myObj->nome = textBox2->Text;
myObj->voto_diploma = Convert::ToInt32(textBox35->Text);
primo->Add(myObj);
//...
TextWriter ^tw = gcnew StreamWriter(L"primoAnno.txt", true);
for each(MyClass^ obj in primo)
{
//You can use any character or string as separator,
//as long as it's not supposed to appear in the strings.
//Here, I used pipes.
tw->Write(obj->cognome);
tw->Write(L"|");
tw->Write(obj->nome);
tw->Write(L"|");
tw->WriteLine(obj->voto_diploma);
}
tw->Close();
READ
MyClass^ ParseMyClass(String^ line)
{
array<String^>^ splitString = line->Split(L'|');
MyClass^ myObj = gcnew MyClass();
myObj->cognome = splitString[0];
myObj->nome = splitString[1];
myObj->voto_diploma = Convert::ToInt32(splitString[2]);
return myObj;
}
DELETE
TextWriter^ tw = gcnew StreamWriter(L"primoAnno2.txt", true);
TextReader^ tr = gcnew StreamReader(L"primoAnno.txt");
String^ line;
while((line=tr->ReadLine()) != nullptr)
{
MyClass^ obj = ParseMyClass(line);
if(obj->cognome != L"cat")
tw->WriteLine(line);
}
tr->Close();
tw->Close();
File::Delete(L"primoAnno.txt");
File::Move(L"primoAnno2.txt", L"primoAnno.txt");
but the deleting part doesnt work properly. Can you please help me to fix it?
As far as i can tell, the function for deleting only checks 'cognome' so if the word you are trying to match is in fact 'nome' it will not match. Try modifying the delete function like this:
if(obj->cognome != L"cat" && obj->nome != L"cat" )
tw->WriteLine(line);
}