2013-01-24 21:30:11 +00:00
|
|
|
/*
|
|
|
|
* Rufus: The Reliable USB Formatting Utility
|
|
|
|
* Standard Windows function calls
|
2015-01-01 23:39:28 +00:00
|
|
|
* Copyright © 2013-2015 Pete Batard <pete@akeo.ie>
|
2013-01-24 21:30:11 +00:00
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
#ifdef _CRTDBG_MAP_ALLOC
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <crtdbg.h>
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#include <windows.h>
|
|
|
|
#include <sddl.h>
|
|
|
|
|
|
|
|
#include "msapi_utf8.h"
|
|
|
|
#include "rufus.h"
|
2013-10-15 21:58:27 +00:00
|
|
|
#include "resource.h"
|
2015-01-25 00:56:38 +00:00
|
|
|
#include "settings.h"
|
2013-10-15 21:58:27 +00:00
|
|
|
#include "localization.h"
|
2013-01-24 21:30:11 +00:00
|
|
|
|
2013-11-05 02:16:49 +00:00
|
|
|
int nWindowsVersion = WINDOWS_UNDEFINED;
|
|
|
|
char WindowsVersionStr[128] = "Windows ";
|
2013-01-24 21:30:11 +00:00
|
|
|
|
2014-05-17 23:37:01 +00:00
|
|
|
/*
|
|
|
|
* Hash table functions - modified From glibc 2.3.2:
|
|
|
|
* [Aho,Sethi,Ullman] Compilers: Principles, Techniques and Tools, 1986
|
|
|
|
* [Knuth] The Art of Computer Programming, part 3 (6.4)
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
* For the used double hash method the table size has to be a prime. To
|
|
|
|
* correct the user given table size we need a prime test. This trivial
|
|
|
|
* algorithm is adequate because the code is called only during init and
|
|
|
|
* the number is likely to be small
|
|
|
|
*/
|
|
|
|
static uint32_t isprime(uint32_t number)
|
|
|
|
{
|
|
|
|
// no even number will be passed
|
|
|
|
uint32_t divider = 3;
|
|
|
|
|
|
|
|
while((divider * divider < number) && (number % divider != 0))
|
|
|
|
divider += 2;
|
|
|
|
|
|
|
|
return (number % divider != 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Before using the hash table we must allocate memory for it.
|
|
|
|
* We allocate one element more as the found prime number says.
|
|
|
|
* This is done for more effective indexing as explained in the
|
|
|
|
* comment for the hash function.
|
|
|
|
*/
|
|
|
|
BOOL htab_create(uint32_t nel, htab_table* htab)
|
|
|
|
{
|
|
|
|
if (htab == NULL) {
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
if (htab->table != NULL) {
|
|
|
|
uprintf("warning: htab_create() was called with a non empty table");
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Change nel to the first prime number not smaller as nel.
|
|
|
|
nel |= 1;
|
|
|
|
while(!isprime(nel))
|
|
|
|
nel += 2;
|
|
|
|
|
|
|
|
htab->size = nel;
|
|
|
|
htab->filled = 0;
|
|
|
|
|
|
|
|
// allocate memory and zero out.
|
|
|
|
htab->table = (htab_entry*)calloc(htab->size + 1, sizeof(htab_entry));
|
|
|
|
if (htab->table == NULL) {
|
|
|
|
uprintf("could not allocate space for hash table\n");
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
return TRUE;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* After using the hash table it has to be destroyed. */
|
|
|
|
void htab_destroy(htab_table* htab)
|
|
|
|
{
|
|
|
|
size_t i;
|
|
|
|
|
|
|
|
if ((htab == NULL) || (htab->table == NULL)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (i=0; i<htab->size+1; i++) {
|
|
|
|
if (htab->table[i].used) {
|
|
|
|
safe_free(htab->table[i].str);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
htab->filled = 0; htab->size = 0;
|
|
|
|
safe_free(htab->table);
|
|
|
|
htab->table = NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* This is the search function. It uses double hashing with open addressing.
|
|
|
|
* We use a trick to speed up the lookup. The table is created with one
|
|
|
|
* more element available. This enables us to use the index zero special.
|
|
|
|
* This index will never be used because we store the first hash index in
|
|
|
|
* the field used where zero means not used. Every other value means used.
|
|
|
|
* The used field can be used as a first fast comparison for equality of
|
|
|
|
* the stored and the parameter value. This helps to prevent unnecessary
|
|
|
|
* expensive calls of strcmp.
|
|
|
|
*/
|
|
|
|
uint32_t htab_hash(char* str, htab_table* htab)
|
|
|
|
{
|
|
|
|
uint32_t hval, hval2;
|
|
|
|
uint32_t idx;
|
|
|
|
uint32_t r = 0;
|
|
|
|
int c;
|
|
|
|
char* sz = str;
|
|
|
|
|
|
|
|
if ((htab == NULL) || (htab->table == NULL) || (str == NULL)) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Compute main hash value using sdbm's algorithm (empirically
|
|
|
|
// shown to produce half the collisions as djb2's).
|
|
|
|
// See http://www.cse.yorku.ca/~oz/hash.html
|
|
|
|
while ((c = *sz++) != 0)
|
|
|
|
r = c + (r << 6) + (r << 16) - r;
|
|
|
|
if (r == 0)
|
|
|
|
++r;
|
|
|
|
|
|
|
|
// compute table hash: simply take the modulus
|
|
|
|
hval = r % htab->size;
|
|
|
|
if (hval == 0)
|
|
|
|
++hval;
|
|
|
|
|
|
|
|
// Try the first index
|
|
|
|
idx = hval;
|
|
|
|
|
|
|
|
if (htab->table[idx].used) {
|
|
|
|
if ( (htab->table[idx].used == hval)
|
|
|
|
&& (safe_strcmp(str, htab->table[idx].str) == 0) ) {
|
|
|
|
// existing hash
|
|
|
|
return idx;
|
|
|
|
}
|
2015-05-28 17:47:53 +00:00
|
|
|
// uprintf("hash collision ('%s' vs '%s')\n", str, htab->table[idx].str);
|
2014-05-17 23:37:01 +00:00
|
|
|
|
|
|
|
// Second hash function, as suggested in [Knuth]
|
|
|
|
hval2 = 1 + hval % (htab->size - 2);
|
|
|
|
|
|
|
|
do {
|
|
|
|
// Because size is prime this guarantees to step through all available indexes
|
|
|
|
if (idx <= hval2) {
|
|
|
|
idx = ((uint32_t)htab->size) + idx - hval2;
|
|
|
|
} else {
|
|
|
|
idx -= hval2;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we visited all entries leave the loop unsuccessfully
|
|
|
|
if (idx == hval) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If entry is found use it.
|
|
|
|
if ( (htab->table[idx].used == hval)
|
|
|
|
&& (safe_strcmp(str, htab->table[idx].str) == 0) ) {
|
|
|
|
return idx;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
while (htab->table[idx].used);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Not found => New entry
|
|
|
|
|
|
|
|
// If the table is full return an error
|
|
|
|
if (htab->filled >= htab->size) {
|
|
|
|
uprintf("hash table is full (%d entries)", htab->size);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
safe_free(htab->table[idx].str);
|
|
|
|
htab->table[idx].used = hval;
|
|
|
|
htab->table[idx].str = (char*) malloc(safe_strlen(str)+1);
|
|
|
|
if (htab->table[idx].str == NULL) {
|
|
|
|
uprintf("could not duplicate string for hash table\n");
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
memcpy(htab->table[idx].str, str, safe_strlen(str)+1);
|
|
|
|
++htab->filled;
|
|
|
|
|
|
|
|
return idx;
|
|
|
|
}
|
|
|
|
|
2013-11-23 00:46:06 +00:00
|
|
|
BOOL is_x64(void)
|
2013-01-24 21:30:11 +00:00
|
|
|
{
|
2013-11-05 02:16:49 +00:00
|
|
|
BOOL ret = FALSE;
|
2014-05-18 23:56:17 +00:00
|
|
|
PF_TYPE_DECL(WINAPI, BOOL, IsWow64Process, (HANDLE, PBOOL));
|
2013-11-05 02:16:49 +00:00
|
|
|
// Detect if we're running a 32 or 64 bit system
|
|
|
|
if (sizeof(uintptr_t) < 8) {
|
2014-05-12 21:44:10 +00:00
|
|
|
PF_INIT(IsWow64Process, Kernel32);
|
|
|
|
if (pfIsWow64Process != NULL) {
|
|
|
|
(*pfIsWow64Process)(GetCurrentProcess(), &ret);
|
2013-11-05 02:16:49 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ret = TRUE;
|
|
|
|
}
|
|
|
|
return ret;
|
2013-01-24 21:30:11 +00:00
|
|
|
}
|
|
|
|
|
2013-11-05 02:16:49 +00:00
|
|
|
// From smartmontools os_win32.cpp
|
|
|
|
void GetWindowsVersion(void)
|
2013-06-25 21:50:22 +00:00
|
|
|
{
|
2013-11-05 02:16:49 +00:00
|
|
|
OSVERSIONINFOEXA vi, vi2;
|
|
|
|
const char* w = 0;
|
|
|
|
const char* w64 = "32 bit";
|
|
|
|
char* vptr;
|
|
|
|
size_t vlen;
|
|
|
|
unsigned major, minor;
|
|
|
|
ULONGLONG major_equal, minor_equal;
|
|
|
|
BOOL ws;
|
|
|
|
|
|
|
|
nWindowsVersion = WINDOWS_UNDEFINED;
|
|
|
|
safe_strcpy(WindowsVersionStr, sizeof(WindowsVersionStr), "Windows Undefined");
|
|
|
|
|
|
|
|
memset(&vi, 0, sizeof(vi));
|
|
|
|
vi.dwOSVersionInfoSize = sizeof(vi);
|
|
|
|
if (!GetVersionExA((OSVERSIONINFOA *)&vi)) {
|
|
|
|
memset(&vi, 0, sizeof(vi));
|
|
|
|
vi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOA);
|
|
|
|
if (!GetVersionExA((OSVERSIONINFOA *)&vi))
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (vi.dwPlatformId == VER_PLATFORM_WIN32_NT) {
|
|
|
|
|
|
|
|
if (vi.dwMajorVersion > 6 || (vi.dwMajorVersion == 6 && vi.dwMinorVersion >= 2)) {
|
|
|
|
// Starting with Windows 8.1 Preview, GetVersionEx() does no longer report the actual OS version
|
|
|
|
// See: http://msdn.microsoft.com/en-us/library/windows/desktop/dn302074.aspx
|
2015-01-28 01:06:51 +00:00
|
|
|
// And starting with Windows 10 Preview 2, Windows enforces the use of the application/supportedOS
|
|
|
|
// manifest in order for VerSetConditionMask() to report the ACTUAL OS major and minor...
|
2013-11-05 02:16:49 +00:00
|
|
|
|
|
|
|
major_equal = VerSetConditionMask(0, VER_MAJORVERSION, VER_EQUAL);
|
|
|
|
for (major = vi.dwMajorVersion; major <= 9; major++) {
|
|
|
|
memset(&vi2, 0, sizeof(vi2));
|
|
|
|
vi2.dwOSVersionInfoSize = sizeof(vi2); vi2.dwMajorVersion = major;
|
|
|
|
if (!VerifyVersionInfoA(&vi2, VER_MAJORVERSION, major_equal))
|
|
|
|
continue;
|
|
|
|
if (vi.dwMajorVersion < major) {
|
|
|
|
vi.dwMajorVersion = major; vi.dwMinorVersion = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
minor_equal = VerSetConditionMask(0, VER_MINORVERSION, VER_EQUAL);
|
|
|
|
for (minor = vi.dwMinorVersion; minor <= 9; minor++) {
|
|
|
|
memset(&vi2, 0, sizeof(vi2)); vi2.dwOSVersionInfoSize = sizeof(vi2);
|
|
|
|
vi2.dwMinorVersion = minor;
|
|
|
|
if (!VerifyVersionInfoA(&vi2, VER_MINORVERSION, minor_equal))
|
|
|
|
continue;
|
|
|
|
vi.dwMinorVersion = minor;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (vi.dwMajorVersion <= 0xf && vi.dwMinorVersion <= 0xf) {
|
|
|
|
ws = (vi.wProductType <= VER_NT_WORKSTATION);
|
|
|
|
nWindowsVersion = vi.dwMajorVersion << 4 | vi.dwMinorVersion;
|
|
|
|
switch (nWindowsVersion) {
|
|
|
|
case 0x51: w = "XP";
|
|
|
|
break;
|
|
|
|
case 0x52: w = (!GetSystemMetrics(89)?"2003":"2003_R2");
|
|
|
|
break;
|
|
|
|
case 0x60: w = (ws?"Vista":"2008");
|
|
|
|
break;
|
|
|
|
case 0x61: w = (ws?"7":"2008_R2");
|
|
|
|
break;
|
|
|
|
case 0x62: w = (ws?"8":"2012");
|
|
|
|
break;
|
|
|
|
case 0x63: w = (ws?"8.1":"2012_R2");
|
|
|
|
break;
|
2015-01-28 01:06:51 +00:00
|
|
|
case 0x64: w = (ws?"10 (Preview 1)":"Server 10 (Preview 1)");
|
|
|
|
break;
|
|
|
|
// Starting with Windows 10 Preview 2, the major is the same as the public-facing version
|
|
|
|
case 0xA0: w = (ws?"10":"Server 10");
|
2014-02-22 00:34:52 +00:00
|
|
|
break;
|
2013-11-05 02:16:49 +00:00
|
|
|
default:
|
2015-05-02 13:41:46 +00:00
|
|
|
if (nWindowsVersion < 0x51)
|
2014-02-22 00:34:52 +00:00
|
|
|
nWindowsVersion = WINDOWS_UNSUPPORTED;
|
|
|
|
else
|
2014-11-08 00:23:50 +00:00
|
|
|
w = "11 or later";
|
2013-11-05 02:16:49 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (is_x64())
|
|
|
|
w64 = "64-bit";
|
|
|
|
|
|
|
|
vptr = &WindowsVersionStr[sizeof("Windows ") - 1];
|
|
|
|
vlen = sizeof(WindowsVersionStr) - sizeof("Windows ") - 1;
|
|
|
|
if (!w)
|
|
|
|
safe_sprintf(vptr, vlen, "%s %u.%u %s", (vi.dwPlatformId==VER_PLATFORM_WIN32_NT?"NT":"??"),
|
|
|
|
(unsigned)vi.dwMajorVersion, (unsigned)vi.dwMinorVersion, w64);
|
|
|
|
else if (vi.wServicePackMinor)
|
|
|
|
safe_sprintf(vptr, vlen, "%s SP%u.%u %s", w, vi.wServicePackMajor, vi.wServicePackMinor, w64);
|
|
|
|
else if (vi.wServicePackMajor)
|
|
|
|
safe_sprintf(vptr, vlen, "%s SP%u %s", w, vi.wServicePackMajor, w64);
|
|
|
|
else
|
|
|
|
safe_sprintf(vptr, vlen, "%s %s", w, w64);
|
2013-06-25 21:50:22 +00:00
|
|
|
}
|
|
|
|
|
2013-01-24 21:30:11 +00:00
|
|
|
/*
|
|
|
|
* String array manipulation
|
|
|
|
*/
|
2014-05-17 23:37:01 +00:00
|
|
|
void StrArrayCreate(StrArray* arr, uint32_t initial_size)
|
2013-01-24 21:30:11 +00:00
|
|
|
{
|
|
|
|
if (arr == NULL) return;
|
|
|
|
arr->Max = initial_size; arr->Index = 0;
|
2014-01-22 01:28:25 +00:00
|
|
|
arr->String = (char**)calloc(arr->Max, sizeof(char*));
|
|
|
|
if (arr->String == NULL)
|
2013-01-24 21:30:11 +00:00
|
|
|
uprintf("Could not allocate string array\n");
|
|
|
|
}
|
|
|
|
|
2014-05-17 23:37:01 +00:00
|
|
|
int32_t StrArrayAdd(StrArray* arr, const char* str)
|
2013-01-24 21:30:11 +00:00
|
|
|
{
|
|
|
|
char** old_table;
|
2014-01-22 01:28:25 +00:00
|
|
|
if ((arr == NULL) || (arr->String == NULL))
|
2014-05-17 23:37:01 +00:00
|
|
|
return -1;
|
2013-01-24 21:30:11 +00:00
|
|
|
if (arr->Index == arr->Max) {
|
|
|
|
arr->Max *= 2;
|
2014-01-22 01:28:25 +00:00
|
|
|
old_table = arr->String;
|
|
|
|
arr->String = (char**)realloc(arr->String, arr->Max*sizeof(char*));
|
|
|
|
if (arr->String == NULL) {
|
2013-01-24 21:30:11 +00:00
|
|
|
free(old_table);
|
|
|
|
uprintf("Could not reallocate string array\n");
|
2014-05-17 23:37:01 +00:00
|
|
|
return -1;
|
2013-01-24 21:30:11 +00:00
|
|
|
}
|
|
|
|
}
|
2014-01-22 01:28:25 +00:00
|
|
|
arr->String[arr->Index] = safe_strdup(str);
|
2014-05-17 23:37:01 +00:00
|
|
|
if (arr->String[arr->Index] == NULL) {
|
2013-01-24 21:30:11 +00:00
|
|
|
uprintf("Could not store string in array\n");
|
2014-05-17 23:37:01 +00:00
|
|
|
return -1;
|
2013-01-24 21:30:11 +00:00
|
|
|
}
|
2014-05-17 23:37:01 +00:00
|
|
|
return arr->Index++;
|
2013-01-24 21:30:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void StrArrayClear(StrArray* arr)
|
|
|
|
{
|
|
|
|
size_t i;
|
2014-01-22 01:28:25 +00:00
|
|
|
if ((arr == NULL) || (arr->String == NULL))
|
2013-01-24 21:30:11 +00:00
|
|
|
return;
|
|
|
|
for (i=0; i<arr->Index; i++) {
|
2014-01-22 01:28:25 +00:00
|
|
|
safe_free(arr->String[i]);
|
2013-01-24 21:30:11 +00:00
|
|
|
}
|
|
|
|
arr->Index = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
void StrArrayDestroy(StrArray* arr)
|
|
|
|
{
|
|
|
|
StrArrayClear(arr);
|
|
|
|
if (arr != NULL)
|
2014-01-22 01:28:25 +00:00
|
|
|
safe_free(arr->String);
|
2013-01-24 21:30:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Retrieve the SID of the current user. The returned PSID must be freed by the caller using LocalFree()
|
|
|
|
*/
|
|
|
|
static PSID GetSID(void) {
|
|
|
|
TOKEN_USER* tu = NULL;
|
|
|
|
DWORD len;
|
|
|
|
HANDLE token;
|
|
|
|
PSID ret = NULL;
|
|
|
|
char* psid_string = NULL;
|
|
|
|
|
|
|
|
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
|
|
|
|
uprintf("OpenProcessToken failed: %s\n", WindowsErrorString());
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!GetTokenInformation(token, TokenUser, tu, 0, &len)) {
|
|
|
|
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
|
|
|
|
uprintf("GetTokenInformation (pre) failed: %s\n", WindowsErrorString());
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
tu = (TOKEN_USER*)calloc(1, len);
|
|
|
|
}
|
|
|
|
if (tu == NULL) {
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (GetTokenInformation(token, TokenUser, tu, len, &len)) {
|
|
|
|
/*
|
|
|
|
* now of course, the interesting thing is that if you return tu->User.Sid
|
|
|
|
* but free tu, the PSID pointer becomes invalid after a while.
|
|
|
|
* The workaround? Convert to string then back to PSID
|
|
|
|
*/
|
|
|
|
if (!ConvertSidToStringSidA(tu->User.Sid, &psid_string)) {
|
|
|
|
uprintf("Unable to convert SID to string: %s\n", WindowsErrorString());
|
|
|
|
ret = NULL;
|
|
|
|
} else {
|
|
|
|
if (!ConvertStringSidToSidA(psid_string, &ret)) {
|
|
|
|
uprintf("Unable to convert string back to SID: %s\n", WindowsErrorString());
|
|
|
|
ret = NULL;
|
|
|
|
}
|
|
|
|
// MUST use LocalFree()
|
|
|
|
LocalFree(psid_string);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ret = NULL;
|
|
|
|
uprintf("GetTokenInformation (real) failed: %s\n", WindowsErrorString());
|
|
|
|
}
|
|
|
|
free(tu);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* read or write I/O to a file
|
|
|
|
* buffer is allocated by the procedure. path is UTF-8
|
|
|
|
*/
|
|
|
|
BOOL FileIO(BOOL save, char* path, char** buffer, DWORD* size)
|
|
|
|
{
|
|
|
|
SECURITY_ATTRIBUTES s_attr, *ps = NULL;
|
|
|
|
SECURITY_DESCRIPTOR s_desc;
|
|
|
|
PSID sid = NULL;
|
|
|
|
HANDLE handle;
|
|
|
|
BOOL r;
|
|
|
|
BOOL ret = FALSE;
|
|
|
|
|
|
|
|
// Change the owner from admin to regular user
|
|
|
|
sid = GetSID();
|
|
|
|
if ( (sid != NULL)
|
|
|
|
&& InitializeSecurityDescriptor(&s_desc, SECURITY_DESCRIPTOR_REVISION)
|
|
|
|
&& SetSecurityDescriptorOwner(&s_desc, sid, FALSE) ) {
|
|
|
|
s_attr.nLength = sizeof(SECURITY_ATTRIBUTES);
|
|
|
|
s_attr.bInheritHandle = FALSE;
|
|
|
|
s_attr.lpSecurityDescriptor = &s_desc;
|
|
|
|
ps = &s_attr;
|
|
|
|
} else {
|
|
|
|
uprintf("Could not set security descriptor: %s\n", WindowsErrorString());
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!save) {
|
|
|
|
*buffer = NULL;
|
|
|
|
}
|
|
|
|
handle = CreateFileU(path, save?GENERIC_WRITE:GENERIC_READ, FILE_SHARE_READ,
|
2014-11-11 19:17:39 +00:00
|
|
|
ps, save?CREATE_ALWAYS:OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
|
2013-01-24 21:30:11 +00:00
|
|
|
|
|
|
|
if (handle == INVALID_HANDLE_VALUE) {
|
|
|
|
uprintf("Could not %s file '%s'\n", save?"create":"open", path);
|
|
|
|
goto out;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (save) {
|
|
|
|
r = WriteFile(handle, *buffer, *size, size, NULL);
|
|
|
|
} else {
|
|
|
|
*size = GetFileSize(handle, NULL);
|
|
|
|
*buffer = (char*)malloc(*size);
|
|
|
|
if (*buffer == NULL) {
|
|
|
|
uprintf("Could not allocate buffer for reading file\n");
|
|
|
|
goto out;
|
|
|
|
}
|
|
|
|
r = ReadFile(handle, *buffer, *size, size, NULL);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!r) {
|
|
|
|
uprintf("I/O Error: %s\n", WindowsErrorString());
|
|
|
|
goto out;
|
|
|
|
}
|
|
|
|
|
2015-01-01 23:39:28 +00:00
|
|
|
PrintInfoDebug(0, save?MSG_216:MSG_215, path);
|
2013-01-24 21:30:11 +00:00
|
|
|
ret = TRUE;
|
|
|
|
|
|
|
|
out:
|
|
|
|
CloseHandle(handle);
|
|
|
|
if (!ret) {
|
|
|
|
// Only leave a buffer allocated if successful
|
|
|
|
*size = 0;
|
|
|
|
if (!save) {
|
|
|
|
safe_free(*buffer);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2013-01-25 01:38:10 +00:00
|
|
|
unsigned char* GetResource(HMODULE module, char* name, char* type, const char* desc, DWORD* len, BOOL duplicate)
|
|
|
|
{
|
|
|
|
HGLOBAL res_handle;
|
|
|
|
HRSRC res;
|
|
|
|
unsigned char* p = NULL;
|
|
|
|
|
|
|
|
res = FindResourceA(module, name, type);
|
|
|
|
if (res == NULL) {
|
2015-07-03 22:42:45 +00:00
|
|
|
uprintf("Could not locate resource '%s': %s\n", desc, WindowsErrorString());
|
2013-01-25 01:38:10 +00:00
|
|
|
goto out;
|
|
|
|
}
|
|
|
|
res_handle = LoadResource(module, res);
|
|
|
|
if (res_handle == NULL) {
|
2015-07-03 22:42:45 +00:00
|
|
|
uprintf("Could not load resource '%s': %s\n", desc, WindowsErrorString());
|
2013-01-25 01:38:10 +00:00
|
|
|
goto out;
|
|
|
|
}
|
|
|
|
*len = SizeofResource(module, res);
|
|
|
|
|
|
|
|
if (duplicate) {
|
|
|
|
p = (unsigned char*)malloc(*len);
|
|
|
|
if (p == NULL) {
|
2015-07-03 22:42:45 +00:00
|
|
|
uprintf("Coult not allocate resource '%s'\n", desc);
|
2013-01-25 01:38:10 +00:00
|
|
|
goto out;
|
|
|
|
}
|
|
|
|
memcpy(p, LockResource(res_handle), *len);
|
|
|
|
} else {
|
|
|
|
p = (unsigned char*)LockResource(res_handle);
|
|
|
|
}
|
|
|
|
|
|
|
|
out:
|
|
|
|
return p;
|
|
|
|
}
|
|
|
|
|
2014-12-20 00:22:00 +00:00
|
|
|
DWORD GetResourceSize(HMODULE module, char* name, char* type, const char* desc)
|
|
|
|
{
|
|
|
|
DWORD len = 0;
|
|
|
|
return (GetResource(module, name, type, desc, &len, FALSE) == NULL)?0:len;
|
|
|
|
}
|
2013-01-25 01:38:10 +00:00
|
|
|
|
2015-01-16 01:51:24 +00:00
|
|
|
// Run a console command, with optional redirection of stdout and stderr to our log
|
|
|
|
DWORD RunCommand(const char* cmd, const char* dir, BOOL log)
|
|
|
|
{
|
2015-06-25 18:51:42 +00:00
|
|
|
DWORD ret, dwRead, dwAvail, dwPipeSize = 4096;
|
2015-01-16 01:51:24 +00:00
|
|
|
STARTUPINFOA si = {0};
|
|
|
|
PROCESS_INFORMATION pi = {0};
|
|
|
|
HANDLE hOutputRead = INVALID_HANDLE_VALUE, hOutputWrite = INVALID_HANDLE_VALUE;
|
|
|
|
HANDLE hDupOutputWrite = INVALID_HANDLE_VALUE;
|
2015-06-25 18:51:42 +00:00
|
|
|
static char* output;
|
2015-01-16 01:51:24 +00:00
|
|
|
|
|
|
|
si.cb = sizeof(si);
|
|
|
|
if (log) {
|
2015-08-22 22:23:08 +00:00
|
|
|
// NB: The size of a pipe is a suggestion, NOT an absolute guarantee
|
2015-06-25 18:51:42 +00:00
|
|
|
// This means that you may get a pipe of 4K even if you requested 1K
|
|
|
|
if (!CreatePipe(&hOutputRead, &hOutputWrite, NULL, dwPipeSize)) {
|
2015-01-16 01:51:24 +00:00
|
|
|
ret = GetLastError();
|
|
|
|
uprintf("Could not set commandline pipe: %s", WindowsErrorString());
|
|
|
|
goto out;
|
|
|
|
}
|
|
|
|
// We need an inheritable pipe endpoint handle
|
|
|
|
DuplicateHandle(GetCurrentProcess(), hOutputWrite, GetCurrentProcess(), &hDupOutputWrite,
|
|
|
|
0L, TRUE, DUPLICATE_CLOSE_SOURCE | DUPLICATE_SAME_ACCESS);
|
|
|
|
si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
|
|
|
|
si.wShowWindow = SW_HIDE;
|
|
|
|
si.hStdOutput = hDupOutputWrite;
|
|
|
|
si.hStdError = hDupOutputWrite;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!CreateProcessU(NULL, cmd, NULL, NULL, TRUE,
|
|
|
|
NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW, NULL, dir, &si, &pi)) {
|
|
|
|
ret = GetLastError();
|
2015-06-24 19:00:20 +00:00
|
|
|
uprintf("Unable to launch command '%s': %s", cmd, WindowsErrorString());
|
2015-01-16 01:51:24 +00:00
|
|
|
goto out;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (log) {
|
|
|
|
while (1) {
|
2015-02-13 19:51:30 +00:00
|
|
|
// coverity[string_null]
|
2015-06-25 18:51:42 +00:00
|
|
|
if (PeekNamedPipe(hOutputRead, NULL, dwPipeSize, NULL, &dwAvail, NULL)) {
|
|
|
|
if (dwAvail != 0) {
|
|
|
|
output = malloc(dwAvail + 1);
|
|
|
|
if ((output != NULL) && (ReadFile(hOutputRead, output, dwAvail, &dwRead, NULL)) && (dwRead != 0)) {
|
|
|
|
output[dwAvail] = 0;
|
|
|
|
// coverity[tainted_string]
|
|
|
|
uprintf(output);
|
|
|
|
}
|
|
|
|
free(output);
|
2015-01-16 01:51:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (WaitForSingleObject(pi.hProcess, 0) == WAIT_OBJECT_0)
|
|
|
|
break;
|
|
|
|
Sleep(100);
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!GetExitCodeProcess(pi.hProcess, &ret))
|
|
|
|
ret = GetLastError();
|
|
|
|
CloseHandle(pi.hProcess);
|
|
|
|
CloseHandle(pi.hThread);
|
|
|
|
|
|
|
|
out:
|
|
|
|
safe_closehandle(hDupOutputWrite);
|
|
|
|
safe_closehandle(hOutputRead);
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2015-01-20 21:50:24 +00:00
|
|
|
BOOL CompareGUID(const GUID *guid1, const GUID *guid2) {
|
|
|
|
if ((guid1 != NULL) && (guid2 != NULL)) {
|
|
|
|
return (memcmp(guid1, guid2, sizeof(GUID)) == 0);
|
|
|
|
}
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
2013-01-24 21:30:11 +00:00
|
|
|
/*
|
|
|
|
* Set or restore a Local Group Policy DWORD key indexed by szPath/SzPolicy
|
|
|
|
*/
|
|
|
|
#pragma push_macro("INTERFACE")
|
|
|
|
#undef INTERFACE
|
|
|
|
#define INTERFACE IGroupPolicyObject
|
2015-01-20 21:50:24 +00:00
|
|
|
#define REGISTRY_EXTENSION_GUID { 0x35378EACL, 0x683F, 0x11D2, {0xA8, 0x9A, 0x00, 0xC0, 0x4F, 0xBB, 0xCF, 0xA2} }
|
2013-01-24 21:30:11 +00:00
|
|
|
#define GPO_OPEN_LOAD_REGISTRY 1
|
|
|
|
#define GPO_SECTION_MACHINE 2
|
|
|
|
typedef enum _GROUP_POLICY_OBJECT_TYPE {
|
|
|
|
GPOTypeLocal = 0, GPOTypeRemote, GPOTypeDS
|
|
|
|
} GROUP_POLICY_OBJECT_TYPE, *PGROUP_POLICY_OBJECT_TYPE;
|
|
|
|
DECLARE_INTERFACE_(IGroupPolicyObject, IUnknown) {
|
|
|
|
STDMETHOD(QueryInterface) (THIS_ REFIID riid, LPVOID *ppvObj) PURE;
|
|
|
|
STDMETHOD_(ULONG, AddRef) (THIS) PURE;
|
|
|
|
STDMETHOD_(ULONG, Release) (THIS) PURE;
|
|
|
|
STDMETHOD(New) (THIS_ LPOLESTR pszDomainName, LPOLESTR pszDisplayName, DWORD dwFlags) PURE;
|
|
|
|
STDMETHOD(OpenDSGPO) (THIS_ LPOLESTR pszPath, DWORD dwFlags) PURE;
|
|
|
|
STDMETHOD(OpenLocalMachineGPO) (THIS_ DWORD dwFlags) PURE;
|
|
|
|
STDMETHOD(OpenRemoteMachineGPO) (THIS_ LPOLESTR pszComputerName, DWORD dwFlags) PURE;
|
|
|
|
STDMETHOD(Save) (THIS_ BOOL bMachine, BOOL bAdd,GUID *pGuidExtension, GUID *pGuid) PURE;
|
|
|
|
STDMETHOD(Delete) (THIS) PURE;
|
|
|
|
STDMETHOD(GetName) (THIS_ LPOLESTR pszName, int cchMaxLength) PURE;
|
|
|
|
STDMETHOD(GetDisplayName) (THIS_ LPOLESTR pszName, int cchMaxLength) PURE;
|
|
|
|
STDMETHOD(SetDisplayName) (THIS_ LPOLESTR pszName) PURE;
|
|
|
|
STDMETHOD(GetPath) (THIS_ LPOLESTR pszPath, int cchMaxPath) PURE;
|
|
|
|
STDMETHOD(GetDSPath) (THIS_ DWORD dwSection, LPOLESTR pszPath ,int cchMaxPath) PURE;
|
|
|
|
STDMETHOD(GetFileSysPath) (THIS_ DWORD dwSection, LPOLESTR pszPath, int cchMaxPath) PURE;
|
|
|
|
STDMETHOD(GetRegistryKey) (THIS_ DWORD dwSection, HKEY *hKey) PURE;
|
|
|
|
STDMETHOD(GetOptions) (THIS_ DWORD *dwOptions) PURE;
|
|
|
|
STDMETHOD(SetOptions) (THIS_ DWORD dwOptions, DWORD dwMask) PURE;
|
|
|
|
STDMETHOD(GetType) (THIS_ GROUP_POLICY_OBJECT_TYPE *gpoType) PURE;
|
|
|
|
STDMETHOD(GetMachineName) (THIS_ LPOLESTR pszName, int cchMaxLength) PURE;
|
|
|
|
STDMETHOD(GetPropertySheetPages) (THIS_ HPROPSHEETPAGE **hPages, UINT *uPageCount) PURE;
|
|
|
|
};
|
|
|
|
typedef IGroupPolicyObject *LPGROUPPOLICYOBJECT;
|
|
|
|
|
2014-11-01 22:08:20 +00:00
|
|
|
// I've seen rare cases where pLGPO->lpVtbl->Save(...) gets stuck, which prevents the
|
|
|
|
// application from launching altogether. To alleviate this, use a thread that we can
|
|
|
|
// terminate if needed...
|
|
|
|
typedef struct {
|
|
|
|
BOOL bRestore;
|
|
|
|
BOOL* bExistingKey;
|
|
|
|
const char* szPath;
|
|
|
|
const char* szPolicy;
|
|
|
|
DWORD dwValue;
|
|
|
|
} SetLGP_Params;
|
|
|
|
|
|
|
|
DWORD WINAPI SetLGPThread(LPVOID param)
|
2013-01-24 21:30:11 +00:00
|
|
|
{
|
2014-11-01 22:08:20 +00:00
|
|
|
SetLGP_Params* p = (SetLGP_Params*)param;
|
2013-01-24 21:30:11 +00:00
|
|
|
LONG r;
|
|
|
|
DWORD disp, regtype, val=0, val_size=sizeof(DWORD);
|
|
|
|
HRESULT hr;
|
|
|
|
IGroupPolicyObject* pLGPO;
|
|
|
|
// Along with global 'existing_key', this static value is used to restore initial state
|
|
|
|
static DWORD original_val;
|
|
|
|
HKEY path_key = NULL, policy_key = NULL;
|
|
|
|
// MSVC is finicky about these ones => redefine them
|
|
|
|
const IID my_IID_IGroupPolicyObject =
|
2015-01-20 21:50:24 +00:00
|
|
|
{ 0xea502723L, 0xa23d, 0x11d1, { 0xa7, 0xd3, 0x0, 0x0, 0xf8, 0x75, 0x71, 0xe3 } };
|
2013-01-24 21:30:11 +00:00
|
|
|
const IID my_CLSID_GroupPolicyObject =
|
2015-01-20 21:50:24 +00:00
|
|
|
{ 0xea502722L, 0xa23d, 0x11d1, { 0xa7, 0xd3, 0x0, 0x0, 0xf8, 0x75, 0x71, 0xe3 } };
|
2013-01-24 21:30:11 +00:00
|
|
|
GUID ext_guid = REGISTRY_EXTENSION_GUID;
|
|
|
|
// Can be anything really
|
2015-01-20 21:50:24 +00:00
|
|
|
GUID snap_guid = { 0x3D271CFCL, 0x2BC6, 0x4AC2, {0xB6, 0x33, 0x3B, 0xDF, 0xF5, 0xBD, 0xAB, 0x2A} };
|
2013-01-24 21:30:11 +00:00
|
|
|
|
2014-11-01 22:08:20 +00:00
|
|
|
// Reinitialize COM since it's not shared between threads
|
|
|
|
IGNORE_RETVAL(CoInitializeEx(NULL, COINIT_APARTMENTTHREADED));
|
|
|
|
|
2013-01-24 21:30:11 +00:00
|
|
|
// We need an IGroupPolicyObject instance to set a Local Group Policy
|
|
|
|
hr = CoCreateInstance(&my_CLSID_GroupPolicyObject, NULL, CLSCTX_INPROC_SERVER, &my_IID_IGroupPolicyObject, (LPVOID*)&pLGPO);
|
|
|
|
if (FAILED(hr)) {
|
|
|
|
uprintf("SetLGP: CoCreateInstance failed; hr = %x\n", hr);
|
|
|
|
goto error;
|
|
|
|
}
|
|
|
|
|
|
|
|
hr = pLGPO->lpVtbl->OpenLocalMachineGPO(pLGPO, GPO_OPEN_LOAD_REGISTRY);
|
|
|
|
if (FAILED(hr)) {
|
|
|
|
uprintf("SetLGP: OpenLocalMachineGPO failed - error %x\n", hr);
|
|
|
|
goto error;
|
|
|
|
}
|
|
|
|
|
|
|
|
hr = pLGPO->lpVtbl->GetRegistryKey(pLGPO, GPO_SECTION_MACHINE, &path_key);
|
|
|
|
if (FAILED(hr)) {
|
|
|
|
uprintf("SetLGP: GetRegistryKey failed - error %x\n", hr);
|
|
|
|
goto error;
|
|
|
|
}
|
|
|
|
|
|
|
|
// The DisableSystemRestore is set in Software\Policies\Microsoft\Windows\DeviceInstall\Settings
|
2014-11-01 22:08:20 +00:00
|
|
|
r = RegCreateKeyExA(path_key, p->szPath, 0, NULL, 0, KEY_SET_VALUE | KEY_QUERY_VALUE,
|
2013-01-24 21:30:11 +00:00
|
|
|
NULL, &policy_key, &disp);
|
|
|
|
if (r != ERROR_SUCCESS) {
|
2014-11-01 22:08:20 +00:00
|
|
|
uprintf("SetLGP: Failed to open LGPO path %s - error %x\n", p->szPath, hr);
|
2015-01-23 02:26:41 +00:00
|
|
|
policy_key = NULL;
|
2013-01-24 21:30:11 +00:00
|
|
|
goto error;
|
|
|
|
}
|
|
|
|
|
2014-11-01 22:08:20 +00:00
|
|
|
if ((disp == REG_OPENED_EXISTING_KEY) && (!p->bRestore) && (!(*(p->bExistingKey)))) {
|
2013-01-24 21:30:11 +00:00
|
|
|
// backup existing value for restore
|
2014-11-01 22:08:20 +00:00
|
|
|
*(p->bExistingKey) = TRUE;
|
2013-01-24 21:30:11 +00:00
|
|
|
regtype = REG_DWORD;
|
2014-11-01 22:08:20 +00:00
|
|
|
r = RegQueryValueExA(policy_key, p->szPolicy, NULL, ®type, (LPBYTE)&original_val, &val_size);
|
2013-01-24 21:30:11 +00:00
|
|
|
if (r == ERROR_FILE_NOT_FOUND) {
|
|
|
|
// The Key exists but not its value, which is OK
|
2014-11-01 22:08:20 +00:00
|
|
|
*(p->bExistingKey) = FALSE;
|
2013-01-24 21:30:11 +00:00
|
|
|
} else if (r != ERROR_SUCCESS) {
|
2014-11-01 22:08:20 +00:00
|
|
|
uprintf("SetLGP: Failed to read original %s policy value - error %x\n", p->szPolicy, r);
|
2013-01-24 21:30:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-11-01 22:08:20 +00:00
|
|
|
if ((!p->bRestore) || (*(p->bExistingKey))) {
|
|
|
|
val = (p->bRestore)?original_val:p->dwValue;
|
|
|
|
r = RegSetValueExA(policy_key, p->szPolicy, 0, REG_DWORD, (BYTE*)&val, sizeof(val));
|
2013-01-24 21:30:11 +00:00
|
|
|
} else {
|
2014-11-01 22:08:20 +00:00
|
|
|
r = RegDeleteValueA(policy_key, p->szPolicy);
|
2013-01-24 21:30:11 +00:00
|
|
|
}
|
|
|
|
if (r != ERROR_SUCCESS) {
|
|
|
|
uprintf("SetLGP: RegSetValueEx / RegDeleteValue failed - error %x\n", r);
|
|
|
|
}
|
|
|
|
RegCloseKey(policy_key);
|
|
|
|
policy_key = NULL;
|
|
|
|
|
|
|
|
// Apply policy
|
2014-11-01 22:08:20 +00:00
|
|
|
hr = pLGPO->lpVtbl->Save(pLGPO, TRUE, (p->bRestore)?FALSE:TRUE, &ext_guid, &snap_guid);
|
2014-10-29 19:16:29 +00:00
|
|
|
if (hr != S_OK) {
|
2014-11-01 22:08:20 +00:00
|
|
|
uprintf("SetLGP: Unable to apply %s policy - error %x\n", p->szPolicy, hr);
|
2013-01-24 21:30:11 +00:00
|
|
|
goto error;
|
|
|
|
} else {
|
2014-11-01 22:08:20 +00:00
|
|
|
if ((!p->bRestore) || (*(p->bExistingKey))) {
|
|
|
|
uprintf("SetLGP: Successfully %s %s policy to 0x%08X\n", (p->bRestore)?"restored":"set", p->szPolicy, val);
|
2013-01-24 21:30:11 +00:00
|
|
|
} else {
|
2014-11-01 22:08:20 +00:00
|
|
|
uprintf("SetLGP: Successfully removed %s policy key\n", p->szPolicy);
|
2013-01-24 21:30:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
RegCloseKey(path_key);
|
|
|
|
pLGPO->lpVtbl->Release(pLGPO);
|
|
|
|
return TRUE;
|
|
|
|
|
|
|
|
error:
|
|
|
|
if (path_key != NULL) RegCloseKey(path_key);
|
|
|
|
if (pLGPO != NULL) pLGPO->lpVtbl->Release(pLGPO);
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
#pragma pop_macro("INTERFACE")
|
2014-11-01 22:08:20 +00:00
|
|
|
|
|
|
|
BOOL SetLGP(BOOL bRestore, BOOL* bExistingKey, const char* szPath, const char* szPolicy, DWORD dwValue)
|
|
|
|
{
|
|
|
|
SetLGP_Params params = {bRestore, bExistingKey, szPath, szPolicy, dwValue};
|
|
|
|
DWORD r = FALSE;
|
2015-02-11 23:22:18 +00:00
|
|
|
HANDLE thread_id;
|
|
|
|
|
|
|
|
if (ReadSettingBool(SETTING_DISABLE_LGP)) {
|
|
|
|
uprintf("LPG handling disabled, per settings");
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
|
|
|
|
thread_id = CreateThread(NULL, 0, SetLGPThread, (LPVOID)¶ms, 0, NULL);
|
2014-11-01 22:08:20 +00:00
|
|
|
if (thread_id == NULL) {
|
|
|
|
uprintf("SetLGP: Unable to start thread");
|
|
|
|
return FALSE;
|
|
|
|
}
|
|
|
|
if (WaitForSingleObject(thread_id, 2500) != WAIT_OBJECT_0) {
|
|
|
|
uprintf("SetLGP: Killing stuck thread!");
|
|
|
|
TerminateThread(thread_id, 0);
|
|
|
|
CloseHandle(thread_id);
|
|
|
|
return FALSE;
|
|
|
|
}
|
2015-01-23 02:26:41 +00:00
|
|
|
if (!GetExitCodeThread(thread_id, &r))
|
|
|
|
return FALSE;
|
2014-11-01 22:08:20 +00:00
|
|
|
return (BOOL) r;
|
|
|
|
}
|