• The Old New Thing

    If you're going to throw a badminton match, you have to be less obvious about it

    • 37 Comments

    It may be possible based on your location to view what NBC euphemistically calls "highlights" from the women's badminton doubles match between China's Yu Yang/Wang Xiaoli and South Korea's Jung Kyung Eun/Kim Ha Na. The serves go laughably into the net, there is barely any attempt to chase down shots, and returns go far out of bounds. If this is top-level badminton, I think I could be a world champion.

    Both sides had secured their advance into the next round, and Wired explained why both teams decided that a strategic loss would be advantageous. Julie VanDusky-Allen provides a game-theoretical analysis of the situation. (Even if you randomize the seeds in the knockout portion of the tournament, you can still get into a situation where a team in the round-robin portion of the tournament may decide that it is advantageous to lose a match on purpose.¹)

    Partway into the match, an official warns both teams that if they do not make an effort to win, the teams will both be disqualified. That served to improve the quality of play only marginally.

    Okay, they need to study soccer or American professional basketball, where intentionally losing is a long-standing tradition: You need to make it look like you're trying, or people are going to figure you out. For example, play normally most of the time, but then have a mental lapse and "accidentally" make an error that concedes a point.

    At least fake an injury. That'll let you start playing badly with plausibility.

    (Although these techniques for throwing a match subtly probably don't work if your opponent is also trying to lose.)

    Since the attempt to get both sides to play to win didn't seem to help, perhaps the officials should have announced, "We have decided to assist in motivating the two sides by declaring that the loser of the match will be disqualified from the tournament."

    Now they have something to play for.

    ¹ Consider a four-team group with teams A, B, C, and D. In round 1, A defeats B 5–1 and C defeats D 2–1. In round 2, A defeats D 5–1 and B defeats C 2–1. At this point, A is guaranteed advancement as long as it doesn't lose to C by more than 8 points. If A defeats C, then B will advance. But A may decide that it prefers to play against C in the knockout portion of the tournament. In that case, it can intentionally lose to C in the third round by 4 points (leaving a comfortable margin of error), and as long as B doesn't win by more than 7 points, A will get its wish: C will advance.

  • The Old New Thing

    Don't mention the war. I mentioned it once, but I think I got away with it all right (Episode 2)

    • 18 Comments

    In preparation for the 2012 Olympic Games in London, the official UK tourism bureau produced tips for dealing with people from other countries.

  • The Old New Thing

    Why does my window style change when I call SetWindowText?

    • 9 Comments

    A customer observed some strange behavior with window styles:

    We ran some weird behavior: Calling the Set­Window­Text function causes a change in window styles. Specifically, calling Set­Window­Text results in the WM_STYLE­CHANGING and WM_STYLE­CHANGED messages, and sometimes the result is that the WS_TAB­STOP style is removed. Is this a bug? What would cause this?

    The Set­Window­Text message sends the WM_SET­TEXT message to the control, at which point anything that happens is the window's own responsibility. If it wants to change styles based on the text you sent, then that's what happens. The window manager doesn't do anything special to force it to happen or to prevent it.

    That's weird, because I'm not even listening for WM_SET­TEXT messages. I also verified that there is no call into my code during the call to the the Set­Window­Text function.

    I'm assuming that the window belongs to the same process as the caller. If the window belongs to another process, then the rules are different.

    I'm changing the text of a window created by the same thread.

    Okay, so let's see what we have so far. The customer is calling the Set­Window­Text function to change the text of a window created on the same thread. There is no handler for the WM_SET­TEXT message, and yet the window style is changing. At this point, you might start looking for more obscure sources for the problem, like say a global hook of some sort. While I considered the possibilities, the customer added,

    It may be worth noting that I'm using the Sys­Link.

    Okay, now things are starting to make sense, and it didn't help that the customer provided misleading information in the description of the problem. For example, when the customer wrote, "There is no handler for the WM_SET­TEXT message," the customer was not referring to the window whose window text is changing but to some other unrelated window.

    It's like responding to the statement "A confirmation letter should have been sent to the account holder" with "I never got the confirmation letter," and then the person spends another day trying to figure out why the confirmation letter was never sent before you casually mention, "Oh, I'm not the account holder."

    The WM_SET­TEXT message is sent to the window you passed to Set­Window­Text; in this case, it's the Sys­Link window. It is therefore the window procedure of the Sys­Link window that is relevant here.

    The Sys­Link control remembers whether it was originally created with the WS_TAB­STOP, and if the markup it is given has no tab stops, then it removes the style; if the markup has tab stops, then it re-adds the style.

    How do I add a tab stop to a string? I couldn't find any reference to it and all my guesses failed.

    The tab stops in question are the hyperlinks you added when you used the <A>...</A> notation. If the text has no hyperlinks, then the control removes the WS_TAB­STOP style because it is no longer something you can tab to.

  • The Old New Thing

    How do you prevent the linker from discarding a function you want to make available for debugging?

    • 22 Comments

    We saw some time ago that you can ask the Windows symbolic debugger engine to call a function directly from the debugger. To do this, of course, the function needs to exist.

    But what if you want a function for the sole purpose of debugging? It never gets called from the main program, so the linker will declare the code dead and remove it.

    One sledgehammer solution is to disable discarding of unused functions. This the global solution to a local problem, since you are now preventing the discard of any unused function, even though all you care about is one specific function.

    If you are comfortable hard-coding function decorations for specific architectures, you can use the /INCLUDE directive.

    #if defined(_X86_)
    #define DecorateCdeclFunctionName(fn) "_" #fn
    #elif defined(_AMD64_)
    #define DecorateCdeclFunctionName(fn) #fn
    #elif defined(_IA64_)
    #define DecorateCdeclFunctionName(fn) "." #fn
    #elif defined(_ALPHA_)
    #define DecorateCdeclFunctionName(fn) #fn
    #elif defined(_MIPS_)
    #define DecorateCdeclFunctionName(fn) #fn
    #elif defined(_PPC_)
    #define DecorateCdeclFunctionName(fn) ".." #fn
    #else
    #error Unknown architecture - don't know how it decorates cdecl.
    #endif
    #pragma comment(linker, "/include:" DecoratedCdeclFunctionName(TestMe))
    EXTERN_C void __cdecl TestMe(int x, int y)
    {
        ...
    }
    

    If you are not comfortable with that (and I don't blame you), you can create a false reference to the debugging function that cannot be optimized out. You do this by passing a pointer to the debugging function to a helper function outside your module that doesn't do anything interesting. Since the helper function is not in your module, the compiler doesn't know that the helper function doesn't do anything, so it cannot optimize out the debugging function.

    struct ForceFunctionToBeLinked
    {
      ForceFunctionToBeLinked(const void *p) { SetLastError(PtrToInt(p)); }
    };
    
    ForceFunctionToBeLinked forceTestMe(TestMe);
    

    The call to Set­Last­Error merely updates the thread's last-error code, but since this is not called at a time where anybody cares about the last-error code, it is has no meaningful effect. The compiler doesn't know that, though, so it has to generate the code, and that forces the function to be linked.

    The nice thing about this technique is that the optimizer sees that this class has no data members, so no data gets generated into the module's data segment. The not-nice thing about this technique is that it is kind of opaque.

  • The Old New Thing

    The MARGINS parameter to the DwmExtendFrameIntoClientArea function controls how far the frame extends into the client area

    • 8 Comments

    A customer wrote a program that calls Dwm­Extend­Frame­Into­Client­Area to extend the frame over the entire client area, but then discovered that this made programming difficult:

    I have a window which I want to have a glassy border but an opaque body. I made my entire window transparent by calling Dwm­Extend­Frame­Into­Client­Area, and I understand that this means that I am now responsible for managing the alpha channel when drawing so that the body of my window remains opaque while the glassy border is transparent. Since most GDI functions are not alpha-aware, this management is frustrating. Is there a better way? In pictures, I only want the red portion of the diagram below to be on glass; the inside yellow part should be opaque like normal. Is there an API that can do this?

    This customer's excitement about the glass frame is like somebody who buys a pallet of tangerine juice even though he only wanted two glasses. And now he has questions about how to store the rest of the tangerine juice he didn't want.

    This customer, it appears, passed −1 as the MARGINS to Dwm­Extend­Frame­Into­Client­Area which means "Bring it on, baby! Give me all tangerine all the time everywhere!" If you only want the glass to extend into part of your client area, then say so. Set the MARGINS to the thickness of the glass border (the thickness of the red portion of the above diagram).

  • The Old New Thing

    How to rescue a broken stack trace on x64: Recovering the stack pointer

    • 1 Comments

    Recovering a broken stack on x64 machines on Windows is trickier because the x64 uses unwind codes for stack walking rather than a frame pointer chain. When you dump the stack, all you're going to see is return addresses sprinkled in amongst the stack data.

    Begin digression: According to the x64 ABI, each function must begin with a prologue which sets up the stack frame. It traditionally goes something like this:

        push rbx ;; save registers
        push rsi ;; save registers
        push rdi ;; save registers
        sub rsp, 0x20 ;; allocate space for local variables and outbound calls
    

    Suppose we have functions

    void Top(int a, int b)
    {
     int toplocal = b + 5;
     Middle(a, local);
    }
    
    void Middle(int c, int d)
    {
     Bottom(c+d);
    }
    
    void Bottom(int e)
    {
     int bottomlocal1, bottomlocal2;
     ...
    }
    

    When execution reaches the ... inside function Bottom the stack looks like the following. (I put higher addresses at the top; the stack grows downward. I also assume that the code is compiled with absolutely no optimization.)

          0040F8E8 parameter 4 (unused)
    0040F8E0 parameter 3 (unused)
    0040F8D8 parameter b passed to Top
    0040F8D0 parameter a passed to Top
    Top's stack frame     0040F8C8 return address of Top's caller During execution of Top,
    rsp = 0040F8A0
    0040F8C0 toplocal
        0040F8B8 parameter 4 (unused)
    0040F8B0 parameter 3 (unused)
    0040F8A8 parameter d passed to Middle
    0040F8A0 parameter c passed to Middle
    Middle's stack frame     0040F898 return address of Middle's caller During execution of Middle,
    rsp = 0040F870
    0040F890 padding for alignment
        0040F888 parameter 4 (unused)
    0040F880 parameter 3 (unused)
    0040F878 parameter 2 (unused)
    0040F870 parameter e passed to Bottom
    Bottom's stack frame     0040F868 return address of Bottom's caller During execution of Bottom,
    rsp = 0040F830
    0040F860 padding for alignment
    0040F858 bottomlocal1
    0040F850 bottomlocal2
        0040F848 parameter 4
    0040F840 parameter 3
    0040F838 parameter 2
    0040F830 parameter 1

    Of course, once the optimizer kicks in, there will also be saved registers in the stack frame, the unused space will start getting used as scratch variables, and the parameters will almost certainly not be spilled into their home locations. End digression.

    Consider this crash where we started executing random instructions (data in the code segment) and finally trapped.

    0:000> r
    rax=0000000000000000 rbx=0000000000000005 rcx=0000000000000042
    rdx=0000000000000010 rsi=00000000000615d4 rdi=00000000043f48e0
    rip=0000000000000000 rsp=00000000001ebf68 rbp=00000000043f32d0
     r8=00000000001ebfd0  r9=0000000000000000 r10=000000007fff3cae
    r11=0000000000000000 r12=0000000000000002 r13=0000000000517050
    r14=0000000000000000 r15=00000000043f55c0
    iopl=0         nv up ei pl nz na pe nc
    cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
    ABC!RandomFunction+0x1234:
    00000000`ff6ebaad test    byte ptr [rax+rdx*4],ah ds:00000000`00000040=??
    0:000> k
    Child-SP          RetAddr           Call Site
    00000000`001ebf70 00000000`00000004 ABC!RandomFunction+0x1234
    00000000`001ebf78 00000000`0000000e 0x4
    00000000`001ebf80 00000000`00000000 0xe
    

    Not very helpful. Let's try to reconstruct the call stack. Here's what we have right now:

    001ebf70  00000000`00000004
    001ebf78  00000000`0000000e
    001ebf80  00000000`00000000
    001ebf88  00000000`77ba21bc ntdll!RtlAllocateHeap+0x16c
    001ebf90  00000000`00000000
    001ebf98  00000000`ff6e1fa1 ABC!operator new[]+0x20
    001ebfa0  00000000`00000000
    001ebfa8  00000000`ff6e28ae ABC!DoesUserPreferMetricUnits+0x2a
    001ebfb0  00000000`000615d4
    001ebfb8  00000000`043f48e0
    001ebfc0  00000000`00000002
    001ebfc8  00000000`00517050
    001ebfd0  00000000`00000010
    001ebfd8  00000000`00000000
    001ebfe0  00000000`00000005
    001ebfe8  00000000`ff6e2b9b ABC!CUIController::UpdateTwoLineDisplay+0x156
    001ebff0  00000000`00000002
    001ebff8  00000000`005170f0
    001ec000  00000000`043f55c0
    001ec008  00000000`00510000
    001ec010  00000000`00000000
    001ec018  00000000`00000000
    001ec020  00000000`00000002
    001ec028  00000000`005170f0
    001ec030  00000000`00000000
    001ec038  00000000`00000000
    001ec040  00000000`00000002
    001ec048  00000000`005170f0
    001ec050  00000000`005170f8
    001ec058  00000000`ff6e2a94 ABC!CUIController::displayEvent+0xea
    001ec060  00000000`00750ed0
    001ec070  00000000`00517118
    001ec078  00000000`043f5aa0
    001ec080  00000000`005170f8
    001ec088  00000000`ff6e2f70 ABC!CEventRegistry::fire+0x34
    001ec090  00000000`00518090
    001ec098  00000000`00517118
    001ec0a0  00000000`043f5aa0
    001ec0a8  00000000`0000000e
    001ec0b0  00000000`00000000
    001ec0b8  00000000`00000000
    001ec0c0  00000000`043f2f00
    001ec0c8  00000000`ff6e2eef ABC!CCalculatorState::storeAndFire+0x126
    001ec0d0  00000000`043f5aa0
    001ec0d8  00000000`00000000
    001ec0e0  00000000`001ec180
    001ec0e8  00000000`00000000
    

    (Note that this dump shows addresses increasing downward, whereas the previous diagram had them increasing upward. Being able to read stack dumps comfortably in both directions is one of those skills you develop as you gain experience.)

    There is no frame pointer chain here to help you see if what you found is a call frame. You just have to use your intuition based on the function names. For example, it sounds perfectly reasonable for operator new[] to call Rtl­Allocate­Heap (to allocate memory), but DoesUserPreferMetric­Units is probably not going to call operator new[].

    Some disassembling around of candidate return addresses suggests that the DoesUserPreferMetric­Units is the one likely to have jumped into space, because it is calling through a function pointer variable, whereas the other candidate return addresses used a direct call (or a call to an import table entry, which is unlikely to be invalid).

    How do we reconstruct the stack based on this assumption? You trick the debugger into thinking that execution stopped inside the DoesUserPreferMetric­Units just before or after the fateful jump. It's easier to do "just after", since that's just the return address. We're going to pretend that instead of jumping into space, we jumped to a ret instruction.

    Since we don't know what the junk code did before it finally crashed, the current value of rsp is probably not accurate. We'll have to think backward to a point in time whose stack pointer we can infer, and then replay the code forward.

    From our knowledge of stack frames, we see that the rsp register had the value 001ebfb0 during the execution of DoesUserPreferMetric­Units just before it called the bad function pointer. Let's temporarily set our rsp and rip to simulate the return from the function.

    0:000> r rsp=1ebfb0
    0:000> r rip=ff6e28ae 
    0:000> k
    Child-SP          RetAddr           Call Site
    00000000`001ebfb0 00000000`ff6e2b9b ABC!DoesUserPreferMetricUnits+0x2a
    00000000`001ebff0 00000000`ff6e2a94 ABC!CUIController::UpdateDisplay+0x156
    00000000`001ec060 00000000`ff6e2f70 ABC!CUIController::displayEvent+0xea
    00000000`001ec090 00000000`ff6e2eef ABC!CEventRouter::fire+0x34
    00000000`001ec0d0 00000000`ff6e3469 ABC!CEngineState::storeAndFire+0x126
    00000000`001ec110 00000000`ff6e4149 ABC!CEngine::SetDisplayText+0x39
    00000000`001ec140 00000000`ff6ea48d ABC!CEngine::DisplayResult+0x648
    00000000`001ec3c0 00000000`ff6e49c6 ABC!CEngine::ProcessCommandWorker+0xa1a
    00000000`001ec530 00000000`ff6e4938 ABC!CEngine::ProcessCommand+0x2a
    00000000`001ec560 00000000`ff6e460a ABC!CUIController::ProcessInput+0xaa
    00000000`001ec5a0 00000000`ff6e4744 ABC!CContainer::ProcessInputs+0x7a1
    00000000`001ec700 00000000`77a6c3c1 ABC!CContainer::WndProc+0xa12
    00000000`001ecbe0 00000000`77a6a6d8 USER32!UserCallWinProcCheckWow+0x1ad
    00000000`001ecca0 00000000`77a6a85d USER32!SendMessageWorker+0x682
    00000000`001ecd30 00000000`ff70c5d8 USER32!SendMessageW+0x5c
    00000000`001ecd80 00000000`77a5e53b ABC!CMainDlgFrame::MainDlgProc+0x87
    00000000`001ecdc0 00000000`77a5e2f2 USER32!UserCallDlgProcCheckWow+0x1b6
    00000000`001ece80 00000000`77a5e222 USER32!DefDlgProcWorker+0xf1
    00000000`001ecf00 00000000`77a6c3c1 USER32!DefDlgProcW+0x36
    00000000`001ecf40 00000000`77a6a6d8 USER32!UserCallWinProcCheckWow+0x1ad
    00000000`001ed000 00000000`77a6a85d USER32!SendMessageWorker+0x682
    00000000`001ed090 000007fe`fc890ba3 USER32!SendMessageW+0x5c
    00000000`001ed0e0 000007fe`fc8947e2 COMCTL32!Button_ReleaseCapture+0x157
    00000000`001ed120 00000000`77a6c3c1 COMCTL32!Button_WndProc+0xcde
    00000000`001ed1e0 00000000`77a6c60a USER32!UserCallWinProcCheckWow+0x1ad
    00000000`001ed2a0 00000000`ff6e1a76 USER32!DispatchMessageWorker+0x3b5
    00000000`001ed320 00000000`ff6fa00f ABC!WinMain+0x1db4
    00000000`001efa10 00000000`7794f33d ABC!__mainCRTStartup+0x18e
    00000000`001efad0 00000000`77b82ca1 kernel32!BaseThreadInitThunk+0xd
    00000000`001efb00 00000000`00000000 ntdll!RtlUserThreadStart+0x1d
    0:000> r rsp=001ebf68
    0:000> r rip=ff6ebaad
    

    After getting what we want, we restore the registers to their original values at the time of the crash so that future investigation won't be misled by our editing.

  • The Old New Thing

    I marked my parameter as [optional], so why do I get an RPC error when I pass NULL?

    • 14 Comments

    Consider the following interface declaration in an IDL file:

    // Code in italics is wrong
    
    interface IFoo : IUnknown
    {
        HRESULT Cancel([in, optional, string] LPCWSTR pszReason);
    };
    

    The idea here is that you want to be able to call the Cancel method as pFoo->Cancel(NULL) if you don't want to provide a reason.

    If you try this, you'll find that the call sometimes fails with error 0x800706F4, which decodes to HRESULT_FROM_WIN32(RPC_X_NULL_REF_POINTER). What's going on here?

    The optional attribute does not mean what you think it means. To a C or C++ programmer, an "optional" pointer parameter typically means that it is valid to pass NULL/nullptr as the parameter value. But that's not what it means to the IDL compiler.

    To the IDL compiler, optional parameters are hints to the scripting engine that the parameter should be passed as VT_ERROR/DISP_E_PARAM­NOT­FOUND. The attribute is meaningful only when applied to parameters of type VARIANT or VARIANT*.

    What you actually want is the unique attribute. This somewhat confusingly-named attribute means "The parameter is allowed to be a null pointer." Therefore, the interface should have been written as

    interface IFoo : IUnknown
    {
        HRESULT Cancel([in, unique, string] LPCWSTR pszReason);
    };
    

    At the lowest level in the marshaler, pointer parameters are marked as ref, unique, or ptr. ref parameters may not be null, whereas unique and ptr parameters are allowed to be null. Larry Osterman explained to me that the default for interface pointers (anything derived from IUnknown) is unique and the default for all other pointer types is ref. Therefore, if you want to say that NULL is a valid value for a non-interface pointer parameter, you must say so explicitly by annotating the parameter as [unique].

    It's probably too late to change the behavior of MIDL to reject the [optional] tag on non-VARIANT parameters because in the decades since the attribute was introduced, it's probably being used incorrectly approximately twenty-five bazillion times, and making it an error would break a lot of code. (Even if you just made it a warning, that wouldn't help because a lot of people treat warnings as errors.)

    Exercise: Why is the RPC_X_NULL_REF_POINTER error raised only sometimes?

  • The Old New Thing

    Get Sea-Tac flight information (including gate and baggage claim) via email

    • 3 Comments

    There are lots of flight status sites out there. (My favorite is FlightAware because it's the geekiest of them.) Many of them will send you email alerts when flight information changes, but the one from the Port of Seattle is the only one I know of that will also tell you when the arrival gate and baggage claim carousel number change, which is handy when you're picking up someone at the airport. (Main entry page here.)

    A useful site if you're departing from the airport is the TSA's Security Checkpoint Wait Times site, which gives you estimates of wait times based on data collected in the most recent few weeks.

    When you arrive at Seattle-Tacoma International Airport, a friendly voice welcomes you via a pre-recorded message. Historically, the voice has been that of the mayor of the city of Seattle, but a few years ago, it changed to that of the President of the Port Commission. I don't know what prompted the change, but I guess some sort of gentleman's agreement fell apart.

  • The Old New Thing

    Why is my icon being drawn at the wrong size when I call DrawIcon?

    • 6 Comments

    Some time ago I had a problem with icon drawing. When I tried to draw an icon with Draw­Icon it ended up being drawn at the wrong size. A call to Get­Icon­Info confirmed that the icon was 48×48, but it drew at 32×32.

    The answer is documented in a backwards sort of way in the Draw­Icon­Ex function, which says at the bottom,

    To duplicate DrawIcon (hDC, X, Y, hIcon), call DrawIconEx as follows:

    DrawIconEx (hDC, X, Y, hIcon, 0, 0, 0, NULL,
                DI_NORMAL | DI_COMPAT | DI_DEFAULTSIZE); 
    

    Aha, if you use Draw­Icon, then the icon size is ignored and it is drawn with DI_DEFAULT­SIZE.

    The fix, therefore, was to switch to the Draw­Icon­Ex function so I could remove the DI_DEFAULT­SIZE flag, thereby permitting the icon to be drawn at its actual size.

    - DrawIcon(hdc, pt.x, pt.y, hico);
    + DrawIconEx(hdc, pt.x, pt.y, hico, 0, 0, 0, NULL, DI_NORMAL | DI_COMPAT);
    

    A bonus quirk of the DI_DEFAULT­SIZE flag (and therefore of the Draw­Icon function) is that the drawing is done at the default icon size, even if you asked it to draw a cursor.

  • The Old New Thing

    There is no complete list of all notifications balloon tips in Windows

    • 33 Comments

    A customer wanted a complete list of all notifications balloon tips in Windows.

    There is no such list. Each component is responsible for its own balloon tips and is not required to submit their list of balloon tips to any central authority for cataloging. In order to create such a list, somebody would have to go contact every component team and ask them for a list of all their balloon tips, and that component team would probably undertake a search of their code base looking for balloon tips. And figuring out the text of each balloon tip can be tricky since the text may be built dynamically. (And the customer didn't ask for an explanation of the conditions under which each balloon tip may appear, but that's probably going to be their follow-up question.)

    It's like publishing instructions on how to display a message on the message board, and then somebody asking the message board manufacturer, "Please send me a list of all messages that might appear on the message board." The message board manufacturer doesn't know how its customers are using the message board. They would have to go survey their customers and ask each one to build an inventory of every message that could potentially be shown.

    In other words, this customer was asking for a research project to be spun up to answer their question.

    I suspected that this was a case of the for-if antipattern being applied to custom support questions, and I asked what the customer intended to do with this information.

    It turns out that they didn't even want this list of balloon tips. They just had a couple of balloon tips they were interested in, and they wanted to know if there were settings to disable them.

    But even though that was their stated goal, they still reiterated their request for a list of balloon tips. The customer liaison asked, "Is there a possibility is getting a list of balloon tips generated by Windows itself? Even a partial list would be helpful. Can we help with this?"

    What the customer liaison can do is contact each Windows component team and ask them, "Can you give me a list of the balloon tips your component generates? Even a partial list would be helpful." I wished him luck.

    The customer liaison replied, "I passed this information to the customer and will follow up if they have any further questions." No follow-up ever appeared.

Page 370 of 449 (4,482 items) «368369370371372»