PDFlibPas, losLabin PDF-kehittäjäkirjasto Delphille ja C++Builderille, lähettää luodun PDF:n sähköpostiliitteenä yhdellä tasaisella API-kutsulla, SendDocumentByMail. Windowsissa oletuskuljetus käyttää CDO:ta (Collaboration Data Objects), käyttöjärjestelmään sisäänrakennettua COM-postikomponenttia, ja yksityiskohta, joka todella rikkoo monisäikeiset eräajot, on COM-apartment-alustus, ei SMTP
Tämän API:n takana oleva skenaario on ei-hohdokas ja äärimmäisen yleinen: palvelu renderöi erän kuukauden lopun tiliote-PDF:iä, yhden per asiakas, ja sen on postitettava jokainen ulos ilman ihmistä silmukassa. Työnnä tuo työ säiepooliin läpäisykyvyn vuoksi, ja murto-osa lähetyksistä alkaa epäonnistua COM-virheellä, joka ei koskaan toistu, kun sama koodi ajetaan yhdessä säikeessä. Mikään ei ole vialla SMTP-palvelimessa, PDF:ssä tai liitteessä. Ongelma on se, mitä CoInitializeEx palauttaa säikeessä, jota CDO ei odottanut, ja PDFlibPas on kirjoitettu käsittelemään tuota tapausta tarkoituksella eikä vahingossa
Mitä SendDocumentByMail todella tekee PDFlibPas:n sisällä
SendDocumentByMail on ohut orkestroija, ei postiasiakas itsessään. TPDFlib.SendDocumentByMail tallentaa parhaillaan ladatun asiakirjan omaan väliaikaiseen PDF:ään, paketoi SMTP-asetukset ja viestitekstin TPDFlibMailRequest-tietueeksi, antaa tuon tietueen millä tahansa, joka toteuttaa IPDFlibMailProvider-rajapinnan, ja poistaa väliaikaisen tiedoston uudelleen heti, kun tarjoaja palautuu. Tarjoajarajapinta on todellinen postiasiakas, ja PDFlibPas toimittaa täsmälleen yhden sisäänrakennetun toteutuksen: CDO-pohjaisen tarjoajan, joka kääntyy vain Windowsissa. Kutsu SendDocumentByMail asettamatta MailProvider-ominaisuutta ensin, ja PDFlibPas palautuu tuohon oletukseen automaattisesti. Paluuarvo pysyy tarkoituksella kapeana kauttaaltaan: 1 hyväksytylle, 0 kaikelle muulle, olipa kyse puuttuvasta pakollisesta kentästä, väliaikaistiedoston kirjoitusvirheestä tai tarjoajan hylkäämästä viestistä, todellisen syyn ollessa saatavilla vain GetLastMailError:sta jälkikäteen
var
PDF: TPDFlib;
Sent: Integer;
begin
PDF := TPDFlib.Create; // a new instance already holds one blank document
try
PDF.SetPageDimensions(612, 792); // US Letter, in points
PDF.NewPage;
// ... draw the statement: fonts, text, totals ...
Sent := PDF.SendDocumentByMail(
'smtp.example.com', 0, 1, // port 0 with SSL 1 falls back to 465
'billing@example.com', 'app-password', // SMTP auth
'billing@example.com', 'customer@example.com', '', '',
'Your statement is ready',
'Please find the attached PDF statement.',
'statement-4471.pdf'); // attachment display name
if Sent <> 1 then
Writeln('Send failed: ', PDF.GetLastMailError);
finally
PDF.Free;
end;
end;
Miksi CoInitializeEx palauttaa S_FALSE:n, ja onko se epäonnistuminen?
S_FALSE funktiosta CoInitializeEx ei ole epäonnistuminen, ja koodi, joka kohtelee sitä sellaisena, raportoi epäonnistumisia säikeissä, joissa mikään ei todella mennyt pieleen. CoInitializeEx palauttaa S_OK:n ensimmäisellä kerralla, kun säie onnistuneesti alustaa COM:in, ja se palauttaa S_FALSE:n, kun tuolla säikeellä oli jo COM alustettuna yhteensopivalla samanaikaisuusmallilla, kasvattaen samaa säiekohtaista viittauslaskuria kummassakin tapauksessa, joten molemmat lopputulokset tarvitsevat täsmäävän CoUninitialize-kutsun ennen kuin säie poistuu tai siirtyy toisiinsa liittymättömään työhön. TPDFlib itse noudattaa tätä täsmällistä mallia: TPDFlib-instanssin rakentaminen jo kutsuu CoInitialize-funktiota ja tallentaa, onko täsmäävä CoUninitialize velkaa, käyttäen identtistä S_OK-tai-S_FALSE-tarkistusta. Siihen mennessä, kun SendDocumentByMail saavuttaa CDO-tarjoajansa ja tuo tarjoaja kutsuu CoInitializeEx:ää uudelleen, COM on siis jo alustettu säikeellä tavallisessa tapauksessa, joten tarjoaja lähes aina havaitsee S_FALSE:n eikä S_OK:ta. S_FALSE:n kohteleminen minä tahansa muuna kuin onnistumisena ei ole harvinainen reunatapaus tässä kirjastossa; se on yleinen polku
InitResult := CoInitializeEx(nil, COINIT_APARTMENTTHREADED);
NeedUninitialize := (InitResult = S_OK) or (InitResult = S_FALSE);
if Failed(InitResult) and (InitResult <> RPC_E_CHANGED_MODE) then
begin
ErrorText := 'COM initialization failed';
Exit;
end;
try
// ... create CDO.Message, CDO.Configuration, send ...
finally
if NeedUninitialize then
CoUninitialize;
end;
Miksi CoInitializeEx palauttaa RPC_E_CHANGED_MODE:n?
RPC_E_CHANGED_MODE tarkoittaa, että nykyinen säie alusti COM:in aiemmin eri samanaikaisuusmallilla kuin mitä tämä kutsu pyytää, tyypillisesti koska säie aiemmin siirtyi monisäikeiseen (MTA), ja CDO nyt pyytää yksisäikeisen apartmentin (STA) semantiikkaa COINIT_APARTMENTTHREADED:n kautta. Säie valitsee apartment-mallinsa kerran, eikä mikään voi muuttaa tuota mallia säikeen loppueliniän ajan; CoInitializeEx:n uudelleenyrittäminen eri lipuilla ei korjaa täsmäämättömyyttä, ja CoUninitialize:n kutsuminen ensin purkaisi apartmentin, josta muu tuon säikeen koodi saattaa yhä riippua. PDFlibPas kohtelee RPC_E_CHANGED_MODE:a tilana, jonka kanssa työskennellä, ei virheenä, joka pitäisi raportoida: se ohittaa parillisen CoUninitialize-kutsun, koska kutsu ei koskaan todella hankkinut viittausta vapautettavaksi, ja antaa lähetyksen jatkua olemassa olevalla apartmentilla
RPC_E_CHANGED_MODE näkyy lähes yksinomaan uudelleenkäytetyillä säikeillä: säiepoolin työläisellä, IIS- tai palveluisäntäsäikeellä, tai millä tahansa säikeellä, jossa aiempi koodi kuten ADO tai WMI on jo kutsunut CoInitializeEx:ää COINIT_MULTITHREADED:lla ennen kuin postikoodi pääsi lähellekään sitä. Aivan uusi säie, joka ei tee mitään muuta kuin kutsuu SendDocumentByMail:ia, ei osu tähän polkuun. Työsäie, jota eräajoajastin kierrättää tuhansia kertoja päivässä, ja joka on jaettu muun COM-pohjaisen työn kanssa, osuu siihen ehdottomasti, ja se tekee sen ajoittain, mikä on juuri se malli, joka lähettää ihmiset katsomaan ensin SMTP-palvelinta ja sitten säikeistysmallia
Sähköpostiliitteen pitäminen poissa väärästä hakemistosta
PDFlibPas kirjoittaa jokaisen lähtevän liitteen tuoreeseen hakemistoon, joka on nimetty GUID:n mukaan, jonka se generoi jokaisella SendDocumentByMail-kutsulla, nimenomaan jotta samanaikaiset lähetykset eivät koskaan voi törmätä samaan tiedostonimeen ja jotta liitteen nimi ei voi kävellä ulos tuosta hakemistosta. Liitteenä annettuun nimeen ei luoteta polkuna: se kulkee PLSanitizeAttachmentName:n läpi, joka poistaa minkä tahansa hakemisto-osan, hylkää tyhjän merkkijonon ja erikoisnimet . ja .., ja korvaa jokaisen merkin, jonka Windows kohtelee laittomana tiedostonimessä, sekä minkä tahansa ohjausmerkin, alaviivalla. Syötä sille ..\quarter:report.pdf, osittain hakemistoylitys ja osittain laiton kaksoispiste, ja se, mikä saavuttaa levyn, on quarter_report.pdf: kaikki viimeiseen polkuerottimeen asti hylätään, ja kaksoispiste muuttuu alaviivaksi, koska se ei voi esiintyä Windows-tiedostonimessä
function PLSanitizeAttachmentName(const FileName: WideString): WideString;
var
I, P: Integer;
begin
P := LastDelimiter('/\', string(FileName));
Result := Copy(FileName, P + 1, MaxInt); // strip any directory part
if (Result = '') or (Result = '.') or (Result = '..') then
Result := 'document.pdf';
for I := 1 to Length(Result) do
if (Ord(Result[I]) < 32) or (Pos(Result[I], WideString('<>:"/\|?*')) > 0) then
Result[I] := '_';
end;
Kutsukohtainen omistettu hakemisto ei ole vain siisteyttä. SendDocumentByMail poistaa väliaikaistiedoston ja sen hakemiston finally-lohkossa sen jälkeen, kun viesti on lähetetty, käyttäen täsmälleen samaa polkua, johon se kirjoitti, joten liitteen nimi, joka saavuttaisi tuon koodin puhdistamattomana, ei vain sijoittaisi kirjoitusta väärin. Sama puhdistamaton polku saavuttaisi sitten siivousvaiheen, joka kutsuu DeleteFile:a kysymättä lisää kysymyksiä, ja jaetulla temp-kansiolla kaksi samanaikaista lähetystä voisivat myös hiljaa ylikirjoittaa toistensa liitteen saman nimen alla, ennen kuin kumpikaan toimitus on valmis. Nimen puhdistaminen sulkee ylitystapauksen, ja kutsukohtainen GUID-hakemisto sulkee törmäystapauksen, eikä kumpikaan yksinään olisi riittänyt
COM:in eliniän täsmäyttäminen säikeen elinikään työläispoolissa
Luotettavin korjaus apartment-säikeistysepäonnistumisiin eräpostittajassa on lopettaa jokaisen SendDocumentByMail-kutsun kohteleminen omana eristettynä COM-elinikänään, ja sen sijaan alustaa COM kerran per työsäie, tuon säikeen koko eliniäksi. Työläinen, joka kutsuu CoInitializeEx(nil, COINIT_APARTMENTTHREADED):ää käynnistyessään, pitää tuon apartmentin jokaiselle SendDocumentByMail-kutsulle, jonka se tekee, ja kutsuu CoUninitialize:a täsmälleen kerran poistuessaan, ei koskaan näe RPC_E_CHANGED_MODE:a omista postilähetyksistään, koska mikään muu tuolla säikeellä ei saa tilaisuutta alustaa COM:ia ristiriitaisessa tilassa ensin. Jokainen yksittäinen SendDocumentByMail-kutsu ajaa silti oman CoInitializeEx- ja CoUninitialize-parinsa sisäisesti tämän mallin alla, ja se on harmitonta: kun apartment on jo perustettu työsäikeen toimesta, jokainen noista sisäisistä kutsuista näkee nyt S_FALSE:n, kasvattaa ja vähentää samaa viittauslaskuria, ja jättää työsäikeen oman COM-apartmentin koskemattomaksi
type
TMailWorker = class(TThread)
protected
procedure Execute; override;
end;
procedure TMailWorker.Execute;
var
PDF: TPDFlib;
Job: TStatementJob;
begin
CoInitializeEx(nil, COINIT_APARTMENTTHREADED);
try
while not Terminated do
begin
if not TryGetNextJob(Job) then
Break;
PDF := TPDFlib.Create;
try
BuildStatement(PDF, Job);
if PDF.SendDocumentByMail(Job.Host, 0, 1, Job.User, Job.Pass,
Job.From, Job.Recipient, '', '', Job.Subject, Job.Body,
Job.AttachmentName) <> 1 then
LogFailure(Job, PDF.GetLastMailError);
finally
PDF.Free;
end;
end;
finally
CoUninitialize;
end;
end;
Epäonnistumisten diagnosointi ja testaus ilman elävää postilaatikkoa
GetLastMailError on tämän API:n toinen puolisko, joka kannattaa rakentaa lokitukseen ensimmäisestä päivästä lähtien, koska pelkkä 1-tai-0-paluuarvo ei kerro, oliko epäonnistunut lähetys COM-alustusongelma, SMTP-todennushylkäys vai puuttuva liite. MailProvider-ominaisuus on se, mikä tekee koko polusta testattavan ilman todellista postilaatikkoa: aseta sille IPDFlibMailProvider-toteutus, joka kirjaa pyynnöt niiden lähettämisen sijaan, aja eräajo tuota valeistarjoajaa vasten CI-putkessa, ja samat SendDocumentByMail-kutsupaikat jatkavat toimimista muuttumattomina heti, kun MailProvider jätetään asettamatta ja PDFlibPas palautuu sisäänrakennettuun CDO-kuljetukseen tuotannossa
Eräajo, joka postittaa tiliotteita, harvoin pysähtyy lähettämiseen: sama putki tarvitsee usein validoida ja allekirjoittaa PDF:n ennen kuin se lähtee ulos, mikä käsitellään erikseen artikkelissa yhdenmukaisuus- ja allekirjoitustyöpenkkiartikkeli, koska esitarkistus ja allekirjoituksen vahvistus ovat eri huolenaihe kuin postitoimitus, vaikka molemmat ajettaisiin peräkkäin. Kun postitettavat asiakirjat ovat itsessään suuren yhdistämis- tai jakotyön tulostetta yhden tuoreen luodun PDF:n sijaan, suurten PDF:ien suoran pääsyn opas käsittelee tuon tuotantovaiheen. SendDocumentByMail ja tässä kuvattu postitarjoajamalli ovat osa vakiomuotoista PDFlibPas PDF-kehittäjäkirjastoa Delphille ja C++Builderille, ja tuotesivulla on koko API-viite yhdessä kokeiluversion latauksen kanssa