Bug 1090444 - The Printing Progress dialog does not show progress or close when opened from the child process
Leftovahs!
So here's the scoop - I've got this Printing Progress dialog opened via the ShowProgress IPC call over PPrinting.
A few things here:
- ShowProgress is a sync call returning nothing. We probably need to return something like an nsIWebProgressListener so we can update state to the parent from the child.
- This might tie in with bug 1090439 , because if I introduce some sort of interface to talk directly with dialogs, that means I can also maybe tie running a nested event loop with the lifetime of that protocol actor. That way, when a protocol goes down (because the dialog closed), we maybe stop blocking.
Suppose I have PPrintingDialog…. what would that look like?
Obviously:
StateChange
ProgressChange
DocTitleChange
DocURLChange
child:
__delete__
So let's think this through a scenario. Child calls
nsPrintingPromptServiceProxy's ShowProgress method… we call the
ShowProgress IPC method, and this returns immediately with something
that implements PPrintingDialog.
Whatever that thing is, it also needs to implement
nsIWebProgressListener on the child side. On the parent side, we _get
back_ a nsIWebProgressListener from the dialog, and that's what we have
to connect our stuff to. So the parent implementation of this protocol
needs to know how to wrap a returned nsIWebProgressListener from the
parent, and send it the data that the child sends.
At the same time, I want this thing to not just be for progress
dialogs, if possible. Like, I want it to be usable for non-sync-ifying
those IPC calls to the parent in PPrinting.
So I have this light, generic implementation on the child and parent
side. It just knows how to communicate "I've been closed", with maybe a
reason for closing (cancel vs window close?).
How does that work? Ah… looks like nsPrintEngine::ShowPrintErrorDialog
is called if the rv from ShowPrintDialog is not NS_OK, and can handle:
#define ENTITY_FOR_ERROR(label) \
case NS_ERROR_##label: stringName.AssignLiteral("PERR_" #label); break
ENTITY_FOR_ERROR(GFX_PRINTER_NO_PRINTER_AVAILABLE);
ENTITY_FOR_ERROR(GFX_PRINTER_NAME_NOT_FOUND);
ENTITY_FOR_ERROR(GFX_PRINTER_COULD_NOT_OPEN_FILE);
ENTITY_FOR_ERROR(GFX_PRINTER_STARTDOC);
ENTITY_FOR_ERROR(GFX_PRINTER_ENDDOC);
ENTITY_FOR_ERROR(GFX_PRINTER_STARTPAGE);
ENTITY_FOR_ERROR(GFX_PRINTER_DOC_IS_BUSY);
ENTITY_FOR_ERROR(ABORT);
ENTITY_FOR_ERROR(NOT_AVAILABLE);
ENTITY_FOR_ERROR(NOT_IMPLEMENTED);
ENTITY_FOR_ERROR(OUT_OF_MEMORY);
ENTITY_FOR_ERROR(UNEXPECTED);
default:
ENTITY_FOR_ERROR(FAILURE);
#undef ENTITY_FOR_ERROR
So I need to communicate that sort of thing back.
It looks as if NS_ERROR_ABORT is used to communicate that the window
was closed by something other than pressing OK - like hitting the X on
the window, or hitting cancel. Ok, that's useful information.
So that __delete__ should take some kind of status.
/**
* Shows the print progress dialog
*
* @param parent - a DOM windows the dialog will be parented to
* @param webBrowserPrint - represents the document to be printed
* @param printSettings - PrintSettings for print "job"
* @param openDialogObserver - an observer that will be notifed when the dialog is opened
* @param isForPrinting - true - for printing, false for print preview
* @param webProgressListener - additional listener can be registered for progress notifications
* @param printProgressParams - parameter object for passing progress state
* @param notifyOnOpen - this indicates that the observer will be notified when the progress
*
dialog has been opened. If false is returned it means the observer
*
(usually the caller) shouldn't wait
*
For Print Preview Progress there is intermediate progress
*/
void showProgress(in nsIDOMWindow parent,
in nsIWebBrowserPrint webBrowserPrint,
in nsIPrintSettings printSettings,
in nsIObserver openDialogObserver,
in boolean isForPrinting,
out nsIWebProgressListener webProgressListener,
out nsIPrintProgressParams printProgressParams,
out boolean notifyOnOpen);
This progress dialog is NOT MODAL - and does not spin its own event loop.
At least for the XUL progress dialog (which is really the one I care
about), it's opened with openDialog, without the modal option. So it
returns immediately. Great!
So, let's review:
For the progress window:
nsPrintingPromptServiceProxy showProgress is called.
We resolve the nsIDOMWindow to a PBrowser, and then call PPrinting's
showProgress method, passing isForPrinting and the PBrowser.
On the parent side… open the progress dialog, getting back
webProgressListener and a printProgressParams pointers and a
notifyOnOpen boolean. Also create and pass an observer. Construct a
PPrintingDialog with the webProgressListener and printProgressParams,
and pass the PPrintingDialog, and notifyOnOpen to the child. Also passes
nsresult.
Child gets nsresult status. Assuming NS_SUCCEEDED, also gets
PPrintingDialog, and wraps it to be a nsIWebProgressListener and
nsIPrintProgressParams, and passes it nsIObserver. Returns it and
notifyOnOpen to the caller. The observer might fire when dialog is open.
Parent opens the dialog on the next tick, or at least tries to. If it
fails out, tells PPrintingDialog about it, and the child hears about it,
stops spinning event loop, and returns result.
So the dialog is open, and the child has handed back the caller of
ShowProgress an nsIWebProgressListener and an nsIPrintProgressParams.
Those things receive information, which gets forwarded through
PPrintingDialog.
When the dialog closes, __delete__ the PPrintingDialog.
So, I think that's how the progress dialog will work.
What about the other dialogs?
ShowPrintDialog and ShowPageSetup are both modal.
async PPrintingDialog {
parent:
async StateChange
async ProgressChange
async DocTitleChange
async DocURLChange
child:
__delete__(status, PrintData (optional))
}
nsPrintingPromptServiceProxy showPrintDialog is called.
We resolve the nsIDOMWindow to a PBrowser, then call PPrinting's
showPrintDialog, passing PBrowser and the current nsIPrintSettings as
PrintData.
On the parent side, instantiate a PPrintingDialog, queue opening the
showPrintDialog dialog on next tick, return it to the child. On the
child side, take that PPrintingDialog it gets passed, and spin event
loop until __delete__ gets called.
In parent, once dialog function returns, if success, construct a
PrintData to return, return nsresult and PrintData to child in
__delete__ call.
__delete__ in child is received. Stop blocking. Looks at nsresult, and potentially PrintData and returns to caller.
I think I need to think through the wrapping that showProgress does,
and the blocking on __delete__. Once I've got that figured out, I think I
can say whether or not this is going to work.
Y'know, I think I'm making things too complicated by having a single
protocol. I was originally trying to avoid a separate protocol for the
modal print settings dialog, but it really doesn't have a whole lot in
common with the print progress dialog. I think I'm getting myself set up
for troubles by trying to mix them.
So let's assume two different protocols instead
:
async PPrintProgressDialog {
parent:
async StateChange
async ProgressChange
async DocTitleChange
async DocURLChange
__delete__()
child:
async DialogOpened
}
async PPrintSettingsModalDialog {
child:
__delete__(nsresult status, PrintData)
}
We might be able to re-use this second protocol for ShowPageSetup, by the by.
So let's think through PPrintProgressDialog now in terms of the
wrapping of things and the proxying of nsIWebProgressListener and
nsIPrintProgressParams stuff.
ONCE MORE FROM THE TOP.
nsPrintingPromptServiceProxy showProgress is called.
We resolve the nsIDOMWindow to a PBrowser, and then call PPrinting's
showProgress method, passing isForPrinting and the PBrowser.
On the parent side… create an observer to pass, then open the progress
dialog, getting back webProgressListener and a printProgressParams
pointers and a notifyOnOpen boolean. Construct a PPrintProgressDialog
with the webProgressListener and printProgressParams, and pass the
PPrintingDialog, and notifyOnOpen to the child. Also passes nsresult.
Pseudo-ish code follows
class PrintProgressDialogParent MOZ_FINAL: public PPrintProgressDialogParent,
public nsIObserver {
public:
virtual PrintProgressDialogParent(nsIWebProgressListener* aWPL,
nsIPrintProgressParams* aPPP);
virtual bool
RecvStateChange(long flags, nsresult status);
virtual bool
RecvProgressChange(long curSelfProgress,
long maxSelfProgress,
long curTotalProgress,
long maxTotalProgress);
virtual bool
RecvDocTitleChange(nsAString title);
virtual bool
RecvDocURLChange(nsAString url);
NS_DECL_ISUPPORTS
NS_DECL_NSIOBSERVER
private:
nsCOMPtr<nsIWebProgressListener> mWPL;
nsCOMPtr<nsIPrintProgressParams> mPPP;
};
class PrintProgressDialogChild MOZ_FINAL: public PPrintProgressDialogChild,
public nsIWebProgressListener,
public nsIPrintProgressParams
{
public:
PrintProgressDialogChild(nsIObserver* aOpenObserver);
virtual bool
RecvDialogOpened(); // Calls mObs->Observe
NS_DECL_ISUPPORTS
NS_DECL_NSIWEBPROGRESSLISTENER
NS_DECL_NSIPRINTPROGRESSPARAMS
private:
nsCOMPtr<nsIObserver> mObs;
};
This is what gets passed back to the caller of showProgress. The
implementation methods for nsIWebProgressListener and
nsIPrintProgressParams will call the methods to communicate with the
parent, naturally.
Now let's think about the lifetime of the
PrintProgressDialogParent/Child implementations now. Who controls how
long this thing lives?
My first instinct is to tie the lifetime of the channel to the
open-ness of the dialog - to shut down the protocol channel once the
dialog closes. But I don't think there's a super-easy way of doing that.
Perhaps the better approach would be to tie the lifetime to the
lifetime of nsIWebProgressListener and nsIPrintProgressParams in the
child.
Like, the child implementation of PrintProgressDialogChild _is_ the
nsIWebProgressListener and nsIPrintProgressParams. When the caller drops
the nsCOMPtrs to us, we'll decrement the refcount, and eventually we'll
destruct - at which point we should tell the parent that we destructed.
So, let's suppose that the child drops the references, and we destruct.
On the parent-side, what do we do? We'll drop the references to mWPL
and mPPP… if anything is still depending on them, they'll stay alive,
but they just won't receive any updates. Ok, I think that works.
A problem might arise if we destruct somehow before we're received the
nsIObserver notification… so I think what should probably happen is that
the manager of PPrintProgressDialog, when deleting, doesn't actually do
any deleting, but decrements the refcount. That way, the nsIObserver
will stay alive as long as anybody cares about it.
But what if the channel is down and somehow we get the nsIObserver
notification? I guess on __delete__, we should also set a flag
internally for PrintProgressDialogParent that tells it that it's without
a child, and then if an observer notification comes in, just warn and
return.
Ok, I think that'll work. I think I can implement this now.
Now let's think through PPrintSettingsModalDialog.
showPrintSettings is called.
async PPrintSettingsModalDialog {
child:
__delete__(nsresult status, PrintData)
}
We resolve the nsIDOMWindow to a PBrowser, then call PPrinting's
showPrintDialog, passing PBrowser and the current nsIPrintSettings as
PrintData.
On the parent side, instantiate a PPrintSettingsModalDialog, store it
in the PrintingParent somewhere (I think?)… because opening multiple
print settings dialogs is probably not going to be supported. Why would
we even support that.
Queue opening the showPrintDialog dialog on next tick, return the
showPrintDialog to the child. On the child side, take that
PPrintingDialog it gets passed, and spin event loop until __delete__
gets called.
HOW DOES THIS WORK
On the parent side, once we hit next tick, open the print settings
dialog. Whatever happens, after that function exits, ensure that the
stashed PPrintSettingsModalDialog member exists for PrintingParent, and
then call __delete__ on it, passing the status and PrintData).
So that's all dandy. I just have to figure out this event loop thingy.
Are there examples of us spinning the event loop waiting for a
__delete__?
From smaug:
"Couldn't you create a PrintDialog protocol, and then in __delete__
continue. While you're waiting __delete__ you'd call
NS_ProcessNextEvent"
So in the child, we get that __delete__ call on our implementation of
PPrintSettingsModalDialogChild, and maybe that
PPrintSettingsModalDialogChild...
Wait. Wait, do I even need a protocol here? This is really just a
message that sends a status and PrintData. Why do I need a new protocol?
Can't I just re-use PPrinting?
Just hear me out here, self.
Let's make it so that the child can receive a PrintDialogResult message, that takes an nsresult and a PrintData.
Then, let's make it so that we async call the parent with
ShowPrintDialog, and then nsPrintingPromptServiceProxy spins the event
loop while we wait for nsresult to be set on a member of
nsPrintingPromptServiceProxy.
Parent receives the message, and opens the dialog, and messages down
the result once the function returns, passing back both the nsresult and
the serialized nsIPrintSettings as PrintData.
nsPrintingPromptServiceProxy spins and spins, and when it receives the
PrintDialogResult message, it sets nsresult and PrintData on member
variables and exits. The nested event loop exits as soon as that
happens.
On exit of the loop, we clear our the PrintData and result, deserialize
to an nsIPrintSettings, and return the result and nsIPrintSettings.
Boom, done.
What about showPageSetup? How does that work?
The mechanism is the same, actually. It's the exact same thing. I think that'll work.
Ok, I think this is ready to try. I think this will work.
I can probably do the progress bar patch in this bug, and do the
showPrintDialog thing in the bug having to do with the sync IPC call.
Steps:
Define a new protocol PPrintProgressDialog, managed by PPrinting, with stubs so that we compile
Huh… build failure:
6:51.20 In file included from
/Users/mikeconley/Projects/mozilla-central/embedding/components/printingui/ipc/PrintingParent.cpp:7:
6:51.20 In file included from ../../../../dist/include/mozilla/dom/Element.h:16:
6:51.20 In file included from ../../../../dist/include/mozilla/dom/FragmentOrElement.h:17:
6:51.20 In file included from ../../../../dist/include/nsAttrAndChildArray.h:18:
6:51.20 In file included from ../../../../dist/include/nsAttrName.h:15:
6:51.21 In file included from ../../../../dist/include/mozilla/dom/NodeInfo.h:25:
6:51.21 ../../../../dist/include/nsCycleCollectionParticipant.h:61:35:
error: no member named 'Value' in namespace 'mozilla::embedding::JS';
did you mean '::JS::Value'?
What the hell is this? I didn't change PrintingParent.cpp too much -
just added the PPrintProgressDialogParent construction stuff because of
IPDL management.
Welp… let's see what happens when I start removing the things I added?...
Oh - notice that it's attempting to use the mozilla::embedding
namespace. That looks just flat out wrong - mozilla::embedding::JS,
unsurprisingly, is not a thing that exists.
So somehow, I've changed things so that something we've included is using the mozilla::embedding namespace.
Ok, so I just commented out the introduction of my new protocol, and my
stubs, and of course things compile properly now. It's unfortunate that
it's kind of "all or nothing" - it's hard for me to introduce or remove
just part of this stuff, since it all kinda relies on its other bits.
AHGAGAGG - I forgot to close my namespaces inside
PrintProgressDialogParent.cpp. I found this by splitting out the
PrintProgressDialog stuff into SOURCES instead of UNIFIED_SOURCES.
Ok, back on track now.
YEAH - I can build it. We're in good shape here.
Define PrintProgressDialogParent, using something like the header I laid out above. Stub out the implementation.
Define PrintProgressDialogChild, using something like the header I laid out above. Stub out the implementation.
Do the observer work on the parent side - calling into the actual nsPrintingPromptService::ShowProgress.
So I've got a flaw in my design. PrintProgressDialogParent takes the
nsIWebProgressListener and nsIPrintProgressParams as constructor
arguments, but we need the PrintProgressDialogParent (which implements
nsIObserver) to pass to nsIPrintingPromptService::ShowProgress (which is
what hands us the nsIWebProgressListener and nsIPrintProgressParams!).
Chicken and egg problem. I think I'm going to have to alter the
constructor to not take the nsIWebProgressListener and
nsIPrintProgressParams, and instead, after we call ShowProgress, we
should take the returned nsIWebProgressListener and
nsIPrintProgressParams and pass them to the PrintProgressDialogParent.
Let's think about the lifetime of this thing again… and how does it tie
in with the AllocPPrintProgressDialogParent defined on
PrintingParent?
So jimm and smaug had an interesting idea that make things a little
less complicated - how about instead of instantiating the
PPrintProgressDialog in the parent, I instantiate it in the child, and
pass it to the parent?
How would that work?
nsPrintingPromptServiceProxy showProgress is called.
We resolve the nsIDOMWindow to a PBrowser, instantiate a
PPrintProgressDialog child, and then call PPrinting's showProgress
method, passing isForPrinting, the PBrowser, and the
PPrintProgressDialog.
On the parent side… open the progress dialog by passing it the
PPrintProgressDialog as the observer, and get back webProgressListener
and a printProgressParams pointers and a notifyOnOpen boolean.
Set the nsIWebProgressListener and nsIPrintProgressParams on the PPrintProgressDialog.
Pass notifyOnOpen to the child. Also passes success.
Child gets success status. Assuming true, returns the
PPrintProgressDialog (which implements nsIPrintProgressParams and
nsIWebProgressListener) and notifyOnOpen to the caller. The observer
might fire when dialog is open.
So the dialog is open, and the child has handed back the caller of
ShowProgress an nsIWebProgressListener and an nsIPrintProgressParams.
Those things receive information, which gets forwarded through
PPrintingDialog.
Now let's think about the lifetime of the
PrintProgressDialogParent/Child implementations now. Who controls how
long this thing lives? Let's continue to tie the lifetime to the
lifetime of nsIWebProgressListener and nsIPrintProgressParams in the
child.
Like, the child implementation of PrintProgressDialogChild _is_ the
nsIWebProgressListener and nsIPrintProgressParams. When the caller drops
the nsCOMPtrs to us, we'll decrement the refcount, and eventually we'll
destruct - at which point we should tell the parent that we destructed.
So, let's suppose that the child drops the references, and we
destruct. On the parent-side, what do we do? We'll drop the references
to mWPL and mPPP… if anything is still depending on them, they'll stay
alive, but they just won't receive any updates. Ok, I think that works.
A problem might arise if we destruct somehow before we're received the
nsIObserver notification… so I think what should probably happen is
that the manager of PPrintProgressDialog, when deleting, doesn't
actually do any deleting, but decrements the refcount. That way, the
nsIObserver will stay alive as long as anybody cares about it.
But what if the channel is down and somehow we get the nsIObserver
notification? I guess on __delete__, we should also set a flag
internally for PrintProgressDialogParent that tells it that it's without
a child, and then if an observer notification comes in, just warn and
return.
Phew, ok - I think this strategy might work.
Alright, so I've kinda (kinda?) got things wired up to at least send
progress events over, but now I'm getting a runtime failure that crashes
the tab:
"IPDL protocol error: bad ID for PPrinting
IPDL protocol error: Error deserializing 'PPrintProgressDialogParent'"
So something is wrong with how I've instantiated
PPringProgressDialogChild - maybe I have to send a construction request
to the parent? I'll try that.
Yeah, that worked - but no progress is being sent from the child. Lemme find out why… attach ye olde MSVC debugger...
Ok! We're hooked up! I've got a progress dialog that shows progress, and then closes!
Do the send message work on the child side.
Figure out garbled logging and crash on close.
So we're crashing in the parent process because we're attempting to
ReleaseListeners in nsPrintProgress, and… I think we're attempting to
free something that's already been freed. Like, I think the nsIObserver
has been destroyed already, but it didn't tell everybody. :/
Let me confirm that by putting a breakpoint in the PrintProgressDialogParent destructor...
Ah, yep - so, we delete the PPrintProgressDialogParent, when we should probably be decrementing the refcount instead.
Right, so that's working - and I can confirm that the
PPrintProgressDialogParent/Child are going down at the right time (need
to wait for garbage collection after printing, but it happens).
So that's great!
Oh this is hilarious. This doesn't work, even without e10s, and even without any of my printing work.
Cancelling print jobs from the progress dialog does not work, and has not worked for some time.
Bug 424965 and bug 761008. Yeesh. So, uh, not my division I guess -
though I'll likely have the skills to fix this when I'm done. I might
want to get those bugs backlogged.
Test lifetime scenarios to ensure we're not leaking the world
We seem to be good here. It looks like the PrintProgressDialogParent
needs to wait a while to get garbage collected, but it eventually does.
Alright, posted. Let's see what blassey thinks.
He digs it! Just some formatting issues, and then let's land this
puppy… actually, I might do a try push first, just to be sure.
Phew, glad I did that. Busted build on Linux debug.
Re-push with some includes in the .cpp file...
Guh, and again:
GUH, and AGAIN.
If this fails to build again, I'm booting up my Ubuntu VM and doing this once and for all over there.
Woo, been building for 44 minutes. That's a good sign.
WOO BUILT - aaaaand landed on fx-team.
TODO:
File bug to remove observer from ShowPageSetup call in nsIPrintingPromptService - filed
bug 1100988.