if (target_thread != g_MainThreadID && IsWindowHung(aTargetWindow)) // Calls to IsWindowHung should probably be avoided if the window belongs to our thread. Relies upon short-circuit boolean order.
// This function must be kept thread-safe because it may be called (indirectly) by hook thread too.
// Although this function could be rolled into a generalized version of the EnumWindowsProc(),
// it will perform better this way because there's less checking required and no mode/flag indicator
// is needed inside lParam to indicate which struct element should be searched for. In addition,
// it's more comprehensible this way. lParam is a pointer to the struct rather than just a
// string because we want to give back the HWND of any matching window.
{
// Since WinText and ExcludeText are seldom used in typical scripts, the following buffer
// is put on the stack here rather than on our callers (inside the WindowSearch object),
// which should help conserve stack space on average. Can't use the ws.mCandidateTitle
// buffer because ws.mFindLastMatch might be true, in which case the original title must
// be preserved.
char win_text[WINDOW_TEXT_SIZE];
WindowSearch &ws = *(WindowSearch *)lParam; // For performance and convenience.
if (!(ws.mSettings->DetectHiddenText || IsWindowVisible(aWnd))) // This text element should not be detectible by the script.
return TRUE; // Skip this child and keep enumerating to try to find a match among the other children.
// The below was formerly outsourced to the following function, but since it is only called from here,
// it has been moved inline:
// int GetWindowTextByTitleMatchMode(HWND aWnd, char *aBuf = NULL, int aBufSize = 0)
int text_length = ws.mSettings->TitleFindFast ? GetWindowText(aWnd, win_text, sizeof(win_text))
: GetWindowTextTimeout(aWnd, win_text, sizeof(win_text)); // The slower method that is able to get text from more types of controls (e.g. large edit controls).
// Older idea that for the above that was not adopted:
// Only if GetWindowText() gets 0 length would we try the other method (and of course, don't bother
// using GetWindowTextTimeout() at all if "fast" mode is in effect). The problem with this is that
// many controls always return 0 length regardless of which method is used, so this would slow things
// down a little (but not too badly since GetWindowText() is so much faster than GetWindowTextTimeout()).
// Another potential problem is that some controls may return less text, or different text, when used
// with the fast mode vs. the slow mode (unverified). So it seems best NOT to do this and stick with
// the simple approach above.
if (!text_length) // It has no text (or failure to fetch it).
return TRUE; // Skip this child and keep enumerating to try to find a match among the other children.
// For compatibility with AutoIt v2, strstr() is always used for control/child text elements.
// EXCLUDE-TEXT: The following check takes precedence over the next, so it's done first:
if (*ws.mCriterionExcludeText) // For performance, avoid doing the checks below when blank.
{
if (ws.mSettings->TitleMatchMode == FIND_REGEX)
{
if (RegExMatch(win_text, ws.mCriterionExcludeText))
return FALSE; // Parent can't be a match, so stop searching its children.
}
else // For backward compatibility, all modes other than RegEx behave as follows.
if (strstr(win_text, ws.mCriterionExcludeText))
// Since this child window contains the specified ExcludeText anywhere inside its text,
// the parent window is always a non-match.
return FALSE; // Parent can't be a match, so stop searching its children.
}
// WIN-TEXT:
if (!*ws.mCriterionText) // Match always found in this case. This check is for performance: it avoids doing the checks below when not needed, especially RegEx. Note: It's possible for mCriterionText to be blank, at least when mCriterionExcludeText isn't blank.
{
ws.mFoundChild = aWnd;
return FALSE; // Match found, so stop searching.
}
if (ws.mSettings->TitleMatchMode == FIND_REGEX)
{
if (RegExMatch(win_text, ws.mCriterionText)) // Match found.
{
ws.mFoundChild = aWnd;
return FALSE; // Match found, so stop searching.
}
}
else // For backward compatibility, all modes other than RegEx behave as follows.
if (strstr(win_text, ws.mCriterionText)) // Match found.
{
ws.mFoundChild = aWnd;
return FALSE; // Match found, so stop searching.
}
// UPDATE to the below: The MSDN docs state that EnumChildWindows() already handles the
// recursion for us: "If a child window has created child windows of its own,
// EnumChildWindows() enumerates those windows as well."
// Mostly obsolete comments: Since this child doesn't match, make sure none of its
// children (recursive) match prior to continuing the original enumeration. We don't
// discard the return value from EnumChildWindows() because it's FALSE in two cases:
// 1) The given HWND has no children.
// 2) The given EnumChildProc() stopped prematurely rather than enumerating all the windows.
// and there's no way to distinguish between the two cases without using the
// struct's hwnd because GetLastError() seems to return ERROR_SUCCESS in both
// cases.
//EnumChildWindows(aWnd, EnumChildFind, lParam);
// If matching HWND still hasn't been found, return TRUE to keep searching:
//return ws.mFoundChild == NULL;
return TRUE; // Keep searching.
}
ResultType StatusBarUtil(Var *aOutputVar, HWND aBarHwnd, int aPartNumber, char *aTextToWaitFor
, int aWaitTime, int aCheckInterval)
// aOutputVar is allowed to be NULL if aTextToWaitFor isn't NULL or blank. aBarHwnd is allowed
// to be NULL because in that case, the caller wants us to set ErrorLevel appropriately and also
// make aOutputVar empty.
{
if (aOutputVar)
aOutputVar->Assign(); // Init to blank in case of early return.
// Set default ErrorLevel, which is a special value (2 vs. 1) in the case of StatusBarWait:
// Legacy: Waiting 500ms in place of a "0" seems more useful than a true zero, which doens't need
// to be supported because it's the same thing as something like "IfWinExist":
if (!aWaitTime)
aWaitTime = 500;
if (aCheckInterval < 1)
aCheckInterval = SB_DEFAULT_CHECK_INTERVAL; // Caller relies on us doing this.
if (aPartNumber < 1)
aPartNumber = 1; // Caller relies on us to set default in this case.
// Must have at least one of these. UPDATE: We want to allow this so that the command can be
// used to wait for the status bar text to become blank:
//if (!aOutputVar && !*aTextToWaitFor) return OK;
// Whenever using SendMessageTimeout(), our app will be unresponsive until
// the call returns, since our message loop isn't running. In addition,
// if the keyboard or mouse hook is installed, the input events will lag during
// this call. So keep the timeout value fairly short. Update for v1.0.24:
// There have been at least two reports of the StatusBarWait command ending
// prematurely with an ErrorLevel of 2. The most likely culprit is the below,
// which has now been increased from 100 to 2000:
#define SB_TIMEOUT 2000
HANDLE handle;
LPVOID remote_buf;
LRESULT part_count; // The number of parts this status bar has.
if (!aBarHwnd // These conditions rely heavily on short-circuit boolean order.
|| !SendMessageTimeout(aBarHwnd, SB_GETPARTS, 0, 0, SMTO_ABORTIFHUNG, SB_TIMEOUT, (PDWORD_PTR)&part_count) // It failed or timed out.
|| aPartNumber > part_count
|| !(remote_buf = AllocInterProcMem(handle, WINDOW_TEXT_SIZE + 1, aBarHwnd))) // Alloc mem last.
return OK; // Let ErrorLevel tell the story.
char buf_for_nt[WINDOW_TEXT_SIZE + 1]; // Needed only for NT/2k/XP: the local counterpart to the buf allocated remotely above.
bool is_win9x = g_os.IsWin9x();
char *local_buf = is_win9x ? (char *)remote_buf : buf_for_nt; // Local is the same as remote for Win9x.
DWORD result, start_time;
--aPartNumber; // Convert to zero-based for use below.
// Always do the first iteration so that at least one check is done. Also, start_time is initialized
// unconditionally in the name of code size reduction (it's a low overhead call):
for (*local_buf = '\0', start_time = GetTickCount();;)
{
// MSDN recommends always checking the length of the bar text. It implies that the length is
// unrestricted, so a crash due to buffer overflow could otherwise occur:
if (SendMessageTimeout(aBarHwnd, SB_GETTEXTLENGTH, aPartNumber, 0, SMTO_ABORTIFHUNG, SB_TIMEOUT, &result))
{
// Testing confirms that LOWORD(result) [the length] does not include the zero terminator.
if (LOWORD(result) > WINDOW_TEXT_SIZE) // Text would be too large (very unlikely but good to check for security).
break; // Abort the operation and leave ErrorLevel set to its default to indicate the problem.
// Retrieve the bar's text:
if (SendMessageTimeout(aBarHwnd, SB_GETTEXT, aPartNumber, (LPARAM)remote_buf, SMTO_ABORTIFHUNG, SB_TIMEOUT, &result))
{
if (!is_win9x)
{
if (!ReadProcessMemory(handle, remote_buf, local_buf, LOWORD(result) + 1, NULL)) // +1 to include the terminator (verified: length doesn't include zero terminator).
{
// Fairly critical error (though rare) so seems best to abort.
*local_buf = '\0'; // In case it changed the buf before failing.
break;
}
}
//else Win9x, in which case the local and remote buffers are the same (no copying is needed).
// Check if the retrieved text matches the caller's criteria. In addition to
// normal/intuitive matching, a match is also achieved if both are empty strings.
// In fact, IsTextMatch() yields "true" whenever aTextToWaitFor is the empty string:
WindowSearch &ws = *(WindowSearch *)lParam; // For performance and convenience.
if (*ws.mCriterionClass) // Caller told us to search by class name and number.
{
int length = GetClassName(aWnd, ws.mCandidateTitle, WINDOW_CLASS_SIZE); // Restrict the length to a small fraction of the buffer's size (also serves to leave room to append the sequence number).
// Below: i.e. this control's title (e.g. List) in contained entirely
// within the leading part of the user specified title (e.g. ListBox).
// Even though this is incorrect, the appending of the sequence number
// in the second comparison will weed out any false matches.
// Note: since some controls end in a number (e.g. SysListView32),
// it would not be easy to parse out the user's sequence number to
// simplify/accelerate the search here. So instead, use a method
// more certain to work even though it's a little ugly. It's also
// necessary to do this in a way functionally identical to the below
// so that Window Spy's sequence numbers match the ones generated here:
// Concerning strnicmp(), see lstrcmpi note below for why a locale-insensitive match isn't done instead.
if (length && !strnicmp(ws.mCriterionClass, ws.mCandidateTitle, length)) // Preliminary match of base class name.
{
// mAlreadyVisitedCount was initialized to zero by WindowSearch's constructor. It is used
// to accumulate how many quasi-matches on this class have been found so far. Also,
// comparing ws.mAlreadyVisitedCount to atoi(ws.mCriterionClass + length) would not be
// the same as the below examples such as the following:
// Say the ClassNN being searched for is List01 (where List0 is the class name and 1
// is the sequence number). If a class called "List" exists in the parent window, it
// would be found above as a preliminary match. The below would copy "1" into the buffer,
// which is correctly deemed not to match "01". By contrast, the atoi() method would give
// the wrong result because the two numbers are numerically equal.
_itoa(++ws.mAlreadyVisitedCount, ws.mCandidateTitle, 10); // Overwrite the buffer to contain only the count.
// lstrcmpi() is not used: 1) avoids breaking exisitng scripts; 2) provides consistent behavior
// across multiple locales:
if (!stricmp(ws.mCandidateTitle, ws.mCriterionClass + length)) // The counts match too, so it's a full match.
{
ws.mFoundChild = aWnd; // Save this in here for return to the caller.
return FALSE; // stop the enumeration.
}
}
}
else // Caller told us to search by the text of the control (e.g. the text printed on a button)
{
// Use GetWindowText() rather than GetWindowTextTimeout() because we don't want to find
// the name accidentally in the vast amount of text present in some edit controls (e.g.
// if the script's source code is open for editing in notepad, GetWindowText() would
// likely find an unwanted match for just about anything). In addition,
// GetWindowText() is much faster. Update: Yes, it seems better not to use
// GetWindowTextByTitleMatchMode() in this case, since control names tend to be so
// short (i.e. they would otherwise be very likely to be undesirably found in any large
// edit controls the target window happens to own). Update: Changed from strstr()
// to strncmp() for greater selectivity. Even with this degree of selectivity, it's
// still possible to have ambiguous situations where a control can't be found due
// to its title being entirely contained within that of another (e.g. a button
// with title "Connect" would be found in the title of a button "Connect All").
// The only way to address that would be to insist on an entire title match, but
// that might be tedious if the title of the control is very long. As alleviation,
// the class name + seq. number method above can often be used instead in cases
// of such ambiguity. Update: Using IsTextMatch() now so that user-specified
// TitleMatchMode will be in effect for this also. Also, it's case sensitivity
// helps increase selectivity, which is helpful due to how common short or ambiguous
for (mCriteria = 0, ahk_flag = aTitle, criteria_count = 0;; ++criteria_count, ahk_flag += 4) // +4 only since an "ahk_" string that isn't qualified may have been found.
{
if ( !(ahk_flag = strcasestr(ahk_flag, "ahk_")) ) // No other special strings are present.
{
if (!criteria_count) // Since no special "ahk_" criteria were present, it is CRITERION_TITLE by default.
{
mCriteria = CRITERION_TITLE; // In this case, there is only one criterion.
if (cp = StrChrAny(buf, " \t")) // Group names can't contain spaces, so terminate at the first one to exclude any "ahk_" criteria that come afterward.
*cp = '\0';
if ( !(mCriterionGroup = g_script.FindGroup(buf)) )
return FAIL; // No such group: Inform caller of invalid criteria. No need to do anything else further below.
}
else // It doesn't qualify as a special criteria name even though it starts with "ahk_".
{
--criteria_count; // Decrement criteria_count to compensate for the loop's increment.
continue;
}
// Since above didn't return or continue, a valid "ahk_" criterion has been discovered.
// If this is the first such criterion, any text that lies to its left should be interpreted
// as CRITERION_TITLE. However, for backward compatibility it seems best to disqualify any title
// consisting entirely of whitespace. This is because some scripts might have a variable containing
// whitespace followed by the string ahk_class, etc. (however, any such whitespace is included as a
// literal part of the title criterion for flexibilty and backward compatibility).
if (!criteria_count && ahk_flag > omit_leading_whitespace(aTitle))
{
mCriteria |= CRITERION_TITLE;
// Omit exactly one space or tab from the title criterion. That space or tab is the one
// required to delimit the special "ahk_" string. Any other spaces or tabs to the left of
// that one are considered literal (for flexibility):
size = ahk_flag - aTitle; // This will always be greater than one due to other checks above, which will result in at least one non-whitespace character in the title criterion.
if (size > sizeof(mCriterionTitle)) // Prevent overflow.
size = sizeof(mCriterionTitle);
strlcpy(mCriterionTitle, aTitle, size); // Copy only the eligible substring as the criteria.
mCriterionTitleLength = strlen(mCriterionTitle); // Pre-calculated for performance.
}
}
// Since this function doesn't change mCandidateParent, there is no need to update the candidate's
// attributes unless the type of criterion has changed or if mExcludeTitle became non-blank as
// a result of our action above:
if (mCriteria != orig_criteria || exclude_title_became_non_blank)
UpdateCandidateAttributes(); // In case mCandidateParent isn't NULL, fetch different attributes based on what was set above.
//else for performance reasons, avoid unnecessary updates.
return OK;
}
void WindowSearch::UpdateCandidateAttributes()
// This function must be kept thread-safe because it may be called (indirectly) by hook thread too.
{
// Nothing to do until SetCandidate() is called with a non-NULL candidate and SetCriteria()
// has been called for the first time (otherwise, mCriterionExcludeTitle and other things
// are not yet initialized:
if (!mCandidateParent || !mCriteria)
return;
if ((mCriteria & CRITERION_TITLE) || *mCriterionExcludeTitle) // Need the window's title in both these cases.
if (!GetWindowText(mCandidateParent, mCandidateTitle, sizeof(mCandidateTitle)))
*mCandidateTitle = '\0'; // Failure or blank title is okay.
if (mCriteria & CRITERION_PID) // In which case mCriterionPID should already be filled in, though it might be an explicitly specified zero.
GetClassName(mCandidateParent, mCandidateClass, sizeof(mCandidateClass)); // Limit to WINDOW_CLASS_SIZE in this case since that's the maximum that can be searched.
// Nothing to do for these:
//CRITERION_GROUP: Can't be pre-processed at this stage.
//CRITERION_ID: It is mCandidateParent, which has already been set by SetCandidate().
}
HWND WindowSearch::IsMatch(bool aInvert)
// Caller must have called SetCriteria prior to calling this method, at least for the purpose of setting
// mSettings to a valid address (and possibly other reasons).
// This method returns the HWND of mCandidateParent if it matches the previously specified criteria
// (title/pid/id/class/group) or NULL otherwise. Upon NULL, it doesn't reset mFoundParent or mFoundCount
// in case previous match(es) were found when mFindLastMatch is in effect.
// Thread-safety: With the following exception, this function must be kept thread-safe because it may be
// called (indirectly) by hook thread too: The hook thread must never call here directly or indirectly with
// mArrayStart!=NULL because the corresponding section below is probably not thread-safe.
{
if (!mCandidateParent || !mCriteria) // Nothing to check, so no match.
return NULL;
if ((mCriteria & CRITERION_TITLE) && *mCriterionTitle) // For performance, avoid the calls below (especially RegEx) when mCriterionTitle is blank (assuming it's even possible for it to be blank under these conditions).
{
switch(mSettings->TitleMatchMode)
{
case FIND_ANYWHERE:
if (!strstr(mCandidateTitle, mCriterionTitle)) // Suitable even if mCriterionTitle is blank, though that's already ruled out above.
return NULL;
break;
case FIND_IN_LEADING_PART:
if (strncmp(mCandidateTitle, mCriterionTitle, mCriterionTitleLength)) // Suitable even if mCriterionTitle is blank, though that's already ruled out above. If it were possible, mCriterionTitleLength would be 0 and thus strncmp would yield 0 to indicate "strings are equal".
return NULL;
break;
case FIND_REGEX:
if (!RegExMatch(mCandidateTitle, mCriterionTitle))
return NULL;
break;
default: // Exact match.
if (strcmp(mCandidateTitle, mCriterionTitle))
return NULL;
}
// If above didn't return, it's a match so far so continue onward to the other checks.
}
if (mCriteria & CRITERION_CLASS) // mCriterionClass is probably always non-blank when CRITERION_CLASS is present (harmless even if it isn't), so *mCriterionClass isn't checked.
{
if (mSettings->TitleMatchMode == FIND_REGEX)
{
if (!RegExMatch(mCandidateClass, mCriterionClass))
return NULL;
}
else // For backward compatibility, all other modes use exact-match for Class.
if (strcmp(mCandidateClass, mCriterionClass)) // Doesn't match the required class name.
return NULL;
// If nothing above returned, it's a match so far so continue onward to the other checks.
}
// For the following, mCriterionPID would already be filled in, though it might be an explicitly specified zero.
if ((mCriteria & CRITERION_PID) && mCandidatePID != mCriterionPID) // Doesn't match required PID.
return NULL;
//else it's a match so far, but continue onward in case there are other criteria.
// The following also handles the fact that mCriterionGroup might be NULL if the specified group
// does not exist or was never successfully created:
if ((mCriteria & CRITERION_GROUP) && (!mCriterionGroup || !mCriterionGroup->IsMember(mCandidateParent, *mSettings)))
return NULL; // Isn't a member of specified group.
//else it's a match so far, but continue onward in case there are other criteria (a little strange in this case, but might be useful).
// CRITERION_ID is listed last since in terms of actual calling frequency, this part is hardly ever
// executed: It's only ever called this way from WinActive(), and possibly indirectly by an ahk_group
// that contains an ahk_id specification. It's also called by WinGetList()'s EnumWindows(), though
// extremely rarely. It's also called this way from other places to determine whether an ahk_id window
// matches the other criteria such as WinText, ExcludeTitle, and mAlreadyVisited.
// mCriterionHwnd should already be filled in, though it might be an explicitly specified zero.
// Note: IsWindow(mCriterionHwnd) was already called by SetCriteria().
if ((mCriteria & CRITERION_ID) && mCandidateParent != mCriterionHwnd) // Doesn't match the required HWND.
return NULL;
//else it's a match so far, but continue onward in case there are other criteria.
// The above would have returned if the candidate window isn't a match for what was specified by
// the script's WinTitle parameter. So now check that the ExcludeTitle criterion is satisfied.
// This is done prior to checking WinText/ExcludeText for performance reasons:
if (*mCriterionExcludeTitle)
{
switch(mSettings->TitleMatchMode)
{
case FIND_ANYWHERE:
if (strstr(mCandidateTitle, mCriterionExcludeTitle))
return NULL;
break;
case FIND_IN_LEADING_PART:
if (!strncmp(mCandidateTitle, mCriterionExcludeTitle, mCriterionExcludeTitleLength))
return NULL;
break;
case FIND_REGEX:
if (RegExMatch(mCandidateTitle, mCriterionExcludeTitle))
return NULL;
break;
default: // Exact match.
if (!strcmp(mCandidateTitle, mCriterionExcludeTitle))
return NULL;
}
// If above didn't return, WinTitle and ExcludeTitle are both satisified. So continue
// on below in case there is some WinText or ExcludeText to search.
}
if (!aInvert) // If caller specified aInvert==true, it will do the below instead of us.
for (int i = 0; i < mAlreadyVisitedCount; ++i)
if (mCandidateParent == mAlreadyVisited[i])
return NULL;
if (*mCriterionText || *mCriterionExcludeText) // It's not quite a match yet since there are more criteria.
{
// Check the child windows for the specified criteria.
// EnumChildWindows() will return FALSE (failure) in at least two common conditions:
// 1) It's EnumChildProc callback returned false (i.e. it ended the enumeration prematurely).
// 2) The specified parent has no children.
// Since in both these cases GetLastError() returns ERROR_SUCCESS, we discard the return
// value and just check mFoundChild to determine whether a match has been found:
mFoundChild = NULL; // Init prior to each call, in case mFindLastMatch is true.