• Status: Solved
  • Priority: Medium
  • Security: Public
  • Views: 1449
  • Last Modified:

Question on Madshi Print Monitor

Hi Madshi,

I have been looking at your Print Monitor and so far it works great, even from Win 98 printing to a network printer.

My goal is to be able to count the number of pages printed to each printer.  I ran your demo application printMonitor then printed 1 page in Notepad. The output in printMonitor includes several StartPage, including 2 with the process notepad.exe and 1 with the process spoolsv.exe

Is there a rule that would apply in all cases to know how many pages are printed?

Also, in some cases I would like to cancel the printing after so many pages. Is it still possible to cancel part of a job by the time it shows on printMonitor?

Thank you.
0
moonrise
Asked:
moonrise
  • 17
  • 14
  • 2
  • +1
1 Solution
 
DaFoxCommented:
Hi moonrise,

Have a look at this code, http://www.assarbad.org/stuff/prtmon3vivi.zip. It comes with full source code!! I guess it's easier to solve a problem if you got the entire source.

Regards,
Markus
0
 
MadshiCommented:
>> The output in printMonitor includes several StartPage, including 2 with the process notepad.exe and 1 with the process spoolsv.exe
>> Is there a rule that would apply in all cases to know how many pages are printed?

Maybe there are two StartPage calls in Notepad, but there should only be one StartPage+EndPage combo in Notepad, when you print 1 page. The spoolsv StartPage reflects the work the printer spooler does. If you're not interested in the printer spooler, but in the applications only, just change the InjectLibrary call so that the hook DLL is not injected into system processes, but in applications only.

Generally I can't really say you much about printing. My expert area is API hooking, not printing. I've written this print monitor demo only to demonstrate the power of my API hooking package. This demo shows which application calls which print APIs, nothing more. What you make out of it (e.g. interpreting the StartPage+EndPage calls to find out who prints how many pages) is up to you.

>> Also, in some cases I would like to cancel the printing after so many pages. Is it still possible to cancel part of a job by the time it shows on printMonitor?

Well, I can think of several possible solution on how to cancel an already running print job. Let's say Notepad calls StartPage to print the 3rd page, but you only want to allow 2 pages. Now you could:

(1) Call the usual printer APIs to cancel the job manually. E.g. AbortJob and that stuff.
(2) In the StartPage hook callback function you could *not* call the original API, but just return false. *Hopefully* Notepad will handle the case that StartPage returns false and abort the printing itself. But that's up to how Notepad was programmed. So it will work with some applications and not with others.
(3) You could *not* call the original API, but just return true. Same with all other print APIs, until the job is through. This way Notepad "thinks" it has printed all 100 pages, but actually only the first 2 reach the printer. The remaining 98 pages were swallowed by your hook callback functions. This should work with all applications, if you do it properly.

Maybe there are additional ways to achieve your aim.

P.S: I don't know about that port monitor code Markus linked. Maybe that's also a possibility for you. If it's free and works as good as my print monitor demo, you should surely use the free solution.
0
 
moonriseAuthor Commented:
The other solution proposed does not work with win 98, plus the great advantage I see in your solution is that print jobs can be interrupted easily.

Because i want my application to work the same with local and network printers, it looks like I will have to use what i get from the applications and not from the spooler (only get the StartPage and Endpage from the applicationn not the spooler) for network printers.

I do however have two issues:

1 - when the user sets the number of copies to be printerd > 1 the application still does StartPage only one per page (Spooler does it for each page printed). Do you know of a way to get the number of copies requested from the application.

2 - Does the application find out if the printer is offline or out of paper?

thank you.
0
Technology Partners: We Want Your Opinion!

We value your feedback.

Take our survey and automatically be enter to win anyone of the following:
Yeti Cooler, Amazon eGift Card, and Movie eGift Card!

 
MadshiCommented:
These are print related questions, so again: I'm no expert there. But according to the documentation you can probably either use the API "DocumentProperties" or the API GetPrinter(level2) to get access to the printer's current DEVMODE. Part of that structure is the number of copies.

(2) I'm not sure what you mean there? I guess you can call GetPrinter again to find out whether the printer is offline or not. If the printer is out of paper, probably calling StartDoc will fail. But I'm just guessing here...

Regards, Madshi.
0
 
moonriseAuthor Commented:
Thank you for your help. Now I just have to play a bit more with ala that but I feel confident I'll be able to do what I want.

One little question before I leave you alone with this topic, how do I point to the string values in  (what do I need to do with api to get the string value?)

    with TPrintNotification(pointer(Message.wParam)^) do

      PanelPrintingStatus.Caption := api;
0
 
MadshiCommented:
That code you posted there should be correct. Or what do you mean? I'm not sure whether I understood your question correctly.
0
 
moonriseAuthor Commented:
You're right, I had a mismatch in the record definition TPrintNotification - all fine now
0
 
moonriseAuthor Commented:
hi again,

to stop pages from being printed, I use option 3 above,  *not* call the original API, but just return true.

I thought I could just set a registry value in my main program - a flag called 'Allowed to Print'  - so in HandlePrintNotification(var Message: TMessage);

          if (api = 'CreateDCW') or (api = 'CreateDCA') then
          begin
            if fMaximumPagesPrinted > 0 then
              WriteBool('Allowed to Print', True)
            else
              WriteBool('Allowed to Print', False);
          end;

            if (api = 'EndPage') then
            begin
              Inc(fJobNumberOfPages);

              if fMaximumPagesPrinted < fJobNumberOfPages then
                WriteBool('Allowed to Print', False)
              else
                WriteBool('Allowed to Print', True);
            end;

and then in the hook I have

      if ReadBool('Allowed to Print') then
        result := StartPageNext(dc)
      else
        result := 1;

but that does not seem to work, is there a better way to have the main program pass information to the hook?
0
 
MadshiCommented:
Are you just fooling StartPage? I think you should really fool StartPage + EndPage at least. Maybe some more APIs.

The application is writing that registry value? When does it do that? Can it be a timing problem? Printing might be very fast. It could be that the printing application has finished printing all pages before your application has decided that printing should stop.

Generally misusing the registry is possible, but not nice. Instead have a look at madCodeHook.CreateGlobalFileMapping + MapViewOfFile. The application can write into a shared buffer and all DLL copies can read from it this way.
0
 
moonriseAuthor Commented:
Hi, I got everything working except for the number of copies printed. I get the value from CreateDCACallback or CreateDCWCallback from the DM: PDeviceMode structure - dmCopies but this value is always set to 1074 no matter what. Let me know if you have any idea about this one.


0
 
MadshiCommented:
Is the DM_COPIES flag set in "DM.dmFields"?

Either way, maybe the print code is setting the copies later. Either by calling SetPrinter or by calling DocumentProperties. If I understand the documentation right, both APIs could be used to change the "dmCopies" field.
0
 
MadshiCommented:
So I'd try to hook SetPrinter + DocumentProperties, too.
0
 
moonriseAuthor Commented:
I think you are right - it might be set later. Could you please show me the code to hook SetPrinter + DocumentProperties (I will add 500 points for that)
0
 
MadshiCommented:
Currently I don't have the time to do so. How about trying to get it to work yourself? If you run into problems, just post them here.

You know, if I do the work for you this time, I'll probably also have to do it for you the next time. It's better if you try to do it yourself. Then you learn how it works and can do it all by yourself the next time. At least that's how I see it.
0
 
moonriseAuthor Commented:
You are right, here is what I have done so far - I hope I am on the right track. I have a specific question below - I am not too good with pointers. Thank you.

I added:

  HookAPI('gdi32.dll', 'SetPrinter',@SetPrinterCallback,@SetPrintereNext  );

Added a parameter DM in NotifyApplication to receive the PDeviceMode info
 
procedure NotifyApplication(PrintAPI: TPrintAPI;
                            DeviceA: PChar; DeviceW: PWideChar;
                            DIA: PDocInfoA; DIW: PDocInfoW;
                            DM: PDeviceMode;
                            Result: Boolean);
var

....I have a case in NotifyApplication to do different actions depending on the API - for setprinter I set the number of copies
    paSetPrinter:
      begin
        Copies := DM.dmCopies;
      end;

In SetPrinterNext I only do something if level 2 - I receive a LPBYTE (a pointer to an array of bytes that may contain printer data)

Question: how do I send the PDeviceMode portion from  pPrinter: LPBYTE below to replace the ???? below.

function SetPrinterNext(hPrinter: Handle; Level: DWord; pPrinter: LPBYTE; Command: DWORD): Integer; stdcall;
begin
  Result := SetPrinterNext(hPrinter, Level, pPrinter, Command);

  if Level = 2 then
  begin
    NotifyApplication(paSetPrinter,
                      nil, nil,
                      nil, nil,
                      ???????
                      Result > 0);
  end;
end;

My record sent to my application by SendIpcMessage now looks as follows:

  TPrintNotification = record
    PrintAPI: TPrintAPI;  // This is a list I created instead of using strings
    Params: array [0..MAX_PATH] of char;
    Copies: Integer;
  end;


0
 
MadshiCommented:
I think this one should work:

if (result <> 0) and (Level = 2) and (pPrinter <> nil) then
  begin
    NotifyApplication(paSetPrinter,
                      nil, nil,
                      nil, nil,
                      PPrinterInfo2(pPrinter)^.pDevMode,
                      Result > 0);
  end;

But please note, that you should check "DM.dmFields". SetPrinter might be called with a wild random Copies number. You should evaluate the Copies number only if the DM.dmFields flags say so. This applies to CreateDC, too.
0
 
moonriseAuthor Commented:
Here it is all - does not seem to call SetPrinterCallback - if you have a minute please look over this code - otherwise I appreciate your help so far. This stuff is  bit harder than what I'm used to do.

library HookPrintAPIs;

{$IMAGEBASE $5a000000}

uses Windows, madCodeHook, madStrings, MyTypes, Constants, sysutils, Printers, Winspool;

procedure SetPrintingAllowed(pPrintingAllowed: Boolean); export; forward;

exports
  SetPrintingAllowed index 1;

type
  PHookRec = ^THookRec;
  THookRec = record
    PrintingAllowed: Boolean;
  end;

  TPrintAPI = (paCreateDCA, paCreateDCW, paStartDocA, paStartDocW, paEndDoc, paStartPage, paEndPage, paSetPrinter);

  // This is what the printer hook sends
  TPrintNotification = record
    PrintAPI: TPrintAPI;
    Params: array [0..MAX_PATH] of char;
    Copies: Integer;
  end;

const
  rHookRec: PHookRec = nil;

procedure SetPrintingAllowed(pPrintingAllowed: Boolean);
begin
  rHookRec^.PrintingAllowed := pPrintingAllowed;
end;

procedure NotifyApplication(PrintAPI: TPrintAPI;
                            DeviceA: PChar; DeviceW: PWideChar;
                            DIA: PDocInfoA; DIW: PDocInfoW;
                            DM: PDeviceMode;
                            Result: Boolean);
var
  vPrintNotification: TPrintNotification;
  arrChA  : array [0..MAX_PATH] of char;
  arrChW  : array [0..MAX_PATH] of wideChar;
  Session : dword;
begin
  vPrintNotification.PrintAPI := PrintAPI;

  // Populate some values - not required for all APIs
  case PrintAPI of
    paCreateDCA:
      begin
        if (DeviceA <> nil) then
        begin
          lstrcpyA(arrChA, DeviceA);

          arrChA[11] := #0;
          if lstrcmpA('\\.\DISPLAY', arrChA) = 0 then
            // We don't want to display dcs!
            exit;
          lstrcpyA(vPrintNotification.Params, arrChA);
        end;
      end;
    paCreateDCW:
      begin
        if (DeviceW <> nil) then
        begin
          lstrcpyW(arrChW, DeviceW);

          arrChW[11] := #0;
          if lstrcmpW('\\.\DISPLAY', arrChW) = 0 then
            exit;

          WideToAnsi(DeviceW, arrChA);
          lstrcpyA(vPrintNotification.Params, arrChA);
        end;
      end;
    paStartDocA:
      begin
        if DIA^.lpszDocName <> nil then
          lstrcpyA(vPrintNotification.Params, DIA^.lpszDocName);
      end;
    paStartDocW:
      begin
        if DIW^.lpszDocName <> nil then
        begin
          WideToAnsi(DIW^.lpszDocName, arrChA);
          lstrcpyA(vPrintNotification.Params, arrChA);
        end;
      end;
    paSetPrinter:
      begin
        vPrintNotification.Copies := DM^.dmCopies;
      end;
  end;

  // Which terminal server (XP fast user switching) session shall we contact?
  if AmSystemProcess and (GetCurrentSessionId = 0) then
    // some system process are independent of sessions
    // so let's contact the PrintMonitor application instance
    // which is running in the current input session
    Session := GetInputSessionId
  else
    // we're an application running in a specific session
    // let's contact the PrintMonitor application instance
    // which runs in the same session as we do
    Session := GetCurrentSessionId;

  // Now send the composed strings to our log window
  // hopefully there's an instance running in the specified session
  SendIpcMessage(pchar('PrintMonitor' + IntToStrEx(Session)), @vPrintNotification, sizeOf(vPrintNotification));
end;

var
  CreateDCANext : function (Driver, Device, Output: pchar; dm: PDeviceModeA): dword; stdcall;
  CreateDCWNext : function (Driver, Device, Output: pwidechar; DM: PDeviceModeW): dword; stdcall;
  StartDocANext : function (DC: dword; const DI: TDocInfoA): Integer; stdcall;
  StartDocWNext : function (DC: dword; const DI: TDocInfoW): Integer; stdcall;
  EndDocNext    : function (DC: dword): Integer; stdcall;
  StartPageNext : function (DC: dword): Integer; stdcall;
  EndPageNext   : function (DC: dword): Integer; stdcall;
  SetPrinterNext: function (hPrinter: THandle; Level: DWord; pPrinter: dword; Command: DWORD): Integer; stdcall;

function CreateDCACallback(Driver, Device, Output: PChar; DM: PDeviceModeA): dword; stdcall;
begin
  Result := CreateDCANext(Driver, Device, Output, DM);

  // We log this call only if it is a printer DC creation
  if (Device <> nil) and (not IsBadReadPtr(Device, 1)) and (Device^ <> #0) then
    NotifyApplication(paCreateDCA,
                      Device, nil,
                      nil, nil,
                      nil,
                      False);
end;

function CreateDCWCallback(Driver, Device, Output: PWideChar; DM: PDeviceModeW) : dword; stdcall;
begin
  Result := CreateDCWNext(Driver, Device, Output, DM);

  if (Device <> nil) and (not IsBadReadPtr(Device, 2)) and (Device^ <> #0) then
    NotifyApplication(paCreateDCW,
                      nil, Device,
                      nil, nil,
                      nil,
                      False);
end;

function StartDocACallback(DC: dword; const DI: TDocInfoA): Integer; stdcall;
begin
  Result := StartDocANext(DC, DI);
  NotifyApplication(paStartDocA,
                    nil, nil,
                    @DI, nil,
                    nil,
                    False);
end;

function StartDocWCallback(DC: dword; const DI: TDocInfoW): Integer; stdcall;
begin
  Result := StartDocWNext(DC, DI);
  NotifyApplication(paStartDocW,
                    nil, nil,
                    nil, @DI,
                    nil,
                    False);
end;

function EndDocCallback(DC: dword): Integer; stdcall;
begin
  Result := EndDocNext(DC);
  NotifyApplication(paEndDoc,
                    nil, nil,
                    nil, nil,
                    nil,
                    Result > 0);
end;

function StartPageCallback(dc: dword) : integer; stdcall;
begin
  if rHookRec^.PrintingAllowed then
  begin
    Result := StartPageNext(dc);
    NotifyApplication(paStartPage,
                      nil, nil,
                      nil, nil,
                      nil,
                      Result > 0);
  end
  else
    Result := 1;
end;

function EndPageCallback(dc: dword) : integer; stdcall;
begin
  if rHookRec^.PrintingAllowed then
  begin
    Result := EndPageNext(dc);
    NotifyApplication(paEndPage,
                      nil, nil,
                      nil, nil,
                      nil,
                      result > 0);
  end
  else
    Result := 1;
end;

function SetPrinterCallback(hPrinter: THandle; Level: DWord; pPrinter: dword; Command: DWORD): Integer; stdcall;
var
  vLogFile: TextFile;
begin
  // just to see if it ever goes there - with 'c:\testhook.txt' existing
  AssignFile(vLogFile, 'c:\testhook.txt');
  Append(vLogFile);
  Writeln(vLogFile, 'DM.dmCopies: was here');
  CloseFile(vLogFile);

  Result := SetPrinterNext(hPrinter, Level, pPrinter, Command);

  if (Result <> 0) and (Level = 2) and (pPrinter <> 0) and (PPrinterInfo2(pPrinter)^.pDevMode.dmFields and DM_COPIES > 0) then
  begin
    NotifyApplication(paSetPrinter,
                      nil, nil,
                      nil, nil,
                      PPrinterInfo2(pPrinter)^.pDevMode,
                      Result > 0);
  end;
end;

procedure EntryPointProc(Reason: Integer);
const
  hMapObject: THandle = 0;
begin
  case Reason of
    DLL_PROCESS_ATTACH:
      begin
        Set8087CW($1337);
        hMapObject := CreateFileMapping($FFFFFFFF, nil, PAGE_READWRITE, 0, SizeOf(THookRec), '_HookPrintAPIs');
        rHookRec := MapViewOfFile(hMapObject, FILE_MAP_WRITE, 0, 0, 0);
      end;
    DLL_PROCESS_DETACH:
      begin
        try
          UnMapViewOfFile(rHookRec);
          CloseHandle(hMapObject);
        except
        end;
      end;

    DLL_THREAD_ATTACH:
      begin
      end;

    DLL_THREAD_DETACH:
      begin
      end;
  end;
end;

begin
  // collecting hooks can improve the hook installation performance in win9x
  CollectHooks;
  HookAPI('gdi32.dll', 'CreateDCA', @CreateDCACallback, @CreateDCANext);
  HookAPI('gdi32.dll', 'CreateDCW', @CreateDCWCallback, @CreateDCWNext);
  HookAPI('gdi32.dll', 'StartDocA', @StartDocACallback, @StartDocANext);
  HookAPI('gdi32.dll', 'StartDocW', @StartDocWCallback, @StartDocWNext);
  HookAPI('gdi32.dll', 'EndDoc',    @EndDocCallback,    @EndDocNext   );
  HookAPI('gdi32.dll', 'StartPage', @StartPageCallback, @StartPageNext);
  HookAPI('gdi32.dll', 'EndPage',   @EndPageCallback,   @EndPageNext  );
  HookAPI('gdi32.dll', 'SetPrinter',@SetPrinterCallback,@SetPrinterNext  );
  FlushHooks;

  DllProc := @EntryPointProc;
  EntryPointProc(DLL_PROCESS_ATTACH);
end.
0
 
MadshiCommented:
Please check out the definition of "SetPrinter" in Delphi (e.g. by Ctrl+MouseClick on SetPrinter in your Delphi editor). It's defined in WinSpool.pas like this:

const
   winspl = 'winspool.drv';

[...]

function SetPrinter; external winspl name 'SetPrinterA';
function SetPrinterA; external winspl name 'SetPrinterA';
function SetPrinterW; external winspl name 'SetPrinterW';

So as you can see there is no "gdi32.dll -> SetPrinter" API. So please change your code to this:

HookAPI('winspool.drv', 'SetPrinterA',@SetPrinterACallback,@SetPrinterANext  );
HookAPI('winspool.drv', 'SetPrinterW',@SetPrinterWCallback,@SetPrinterWNext  );
0
 
moonriseAuthor Commented:
Got everything working, # of copies, etc...

But in some cases it seems that the application printing sends pages too fast  and rHookRec^.PrintingAllowed (my file mapping stuff) is not updated fast enough. Adding the Sleep(1) fixes that but sometimes Windows does not like that, especially under win 98. Is there a safe way to slow down this function instead of Sleep(1)?

function StartPageCallback(dc: dword) : integer; stdcall;
begin
  Sleep(1);

  if rHookRec^.PrintingAllowed then
  begin
    Result := StartPageNext(dc);
    NotifyApplication(paStartPage,nil, nil,nil, nil,0,False);
  end
  else
    Result := 1;
end;
0
 
MadshiCommented:
Who sets the "PrintingAllowed := false" and in which situation? Is there no alternative?

What I could imagine would be that the dlls ask the application whether they may print. You could do that by calling SendIpcMessage again, but this time by letting SendIpcMessage wait for an answer before you print.
0
 
moonriseAuthor Commented:
Hi,

I have my last problems resolved by doing all the counting within the DLL and report to the application at the end of the document. Works great except for one combination: Windows 2000 and Network printer where I get memory acces violation error. Will keep working on this one. Again thank you for your help.
0
 
moonriseAuthor Commented:
Just in case you want to look at the crash problem, you can reproduce it as follows on a Win 2K PC:

Create a Netword printer (I just use a generic send to file one)

In your Print Monitor demo, the only change required is in function StartPageCallback(dc: dword) : integer; stdcall;

replace
  result := StartPageNext(dc);
for
  result := 1;

Crashes at least with IE and Notepad
0
 
MadshiCommented:
Hmmm... To be honest: I've no idea why that happens. Seems like a printing related problem. Perhaps you can try giving back "0" indicating a failure?
0
 
moonriseAuthor Commented:
I have tried the 0, same problem. Do you feel that it is a bug in Windows 2000 that we have to live with? It is strange that it only happens with Network printers.
0
 
MadshiCommented:
It's really strange.

How about allowing StartPage, but not letting through EndPage? You should then call AbortDoc (or however that API was called) before closing all the stuff.
0
 
moonriseAuthor Commented:
I have everything working so well I don't think I want to make such a change. Plus sometimes i do allow part of the document to print. What surpises me is that it is the printing application that crashes, like notepad and IE. Does that exclude the possibility of the problem coming from madHook?
0
 
MadshiCommented:
It does not crash, if you allow all printing actions (as far as I understood). That for me almost excludes the possibility of the problem coming from madCodeHook.

The problem with API hooking is always, that you break in to the normal flow of a program. This might sometimes end up in strange behaviour. If you only hook APIs to log something, things are less dangerous. But if you modify the behaviour of APIs, you're always in danger of breaking something.
0
 
MadshiCommented:
I get asked more often lately about how to count pages and that stuff with my print monitor demo. Would you mind sharing some of your work with me? Perhaps I could improve my print monitor demo to the benefit of other madCodeHook customers. Of course you don't need to do it. Just if you like.
0
 
moonriseAuthor Commented:
Because of the type of contract under which I work, I think I better not release too much information about my work. The hard stuff is really the part that you did anyway, the rest anyone can probably easily figure out. I sent you an email about paying for the license. Please let me know if you have not received it.
0
 
MadshiCommented:
Ok, no problem.

P.S: You should have a reply to your mail by now.
0
 
monitorwaCommented:
Hi Moonrise and Madshi,
Im another of those people trying to work out page counting on print jobs! - I have had a read of everything above (that took a while) and downloaed the print monitor code, unfortunitly I am unable to compile it as I require the madhookcode component which ofcourse I would have to buy! Is there a simple sum up of how to accuretly count all the pages to be printed - I need to include when more than one copy is selected. I am happy to make this a question and assign points if its something im likely to get an answer on

thanks

Dave
0
 
MadshiCommented:
This must be a quite old question...   :)

Anyway, you should be able to compile just fine. The madCollection installer contains a fully working evaluation version of madCodeHook. About how to count pages: I don't really know, I've never done that myself. If counting the Start/EndPage calls is not good enough, I don't know what else to do.
0
 
monitorwaCommented:
wow - that was quick, I was in th eprocess of writting you an email and making a new question and then noticed this had arrived?
The start and end will work fine I think but its the issue of multiple copies, I see above that moonrise left happy so it looks like your suggestion did the trick.
The compile err is
[Fatal Error] PrintForm.pas(28): File not found: 'madCodeHook.dcu'
WHich I guess means I need the Madcodehook pas file to add to my lib? open for ideas here

thanks

Dave
0
 
MadshiCommented:
Hmmmm... Which Delphi version are you using? Have you correctly installed madCodeHook? Try the latest beta build, just to be safe:

http://madshi.net/madCollectionBeta.exe

Of course in the component selection screen you need to activate/select madCodeHook for installation. It's not activated by default.
0

Featured Post

Keep up with what's happening at Experts Exchange!

Sign up to receive Decoded, a new monthly digest with product updates, feature release info, continuing education opportunities, and more.

  • 17
  • 14
  • 2
  • +1
Tackle projects and never again get stuck behind a technical roadblock.
Join Now