Clear thread/condvar during shutdown.
[shibboleth/cpp-xmltooling.git] / xmltooling / util / ReloadableXMLFile.cpp
1 /*
2  *  Copyright 2001-2010 Internet2
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 /**
18  * @file ReloadableXMLFile.cpp
19  *
20  * Base class for file-based XML configuration.
21  */
22
23 #include "internal.h"
24 #include "io/HTTPResponse.h"
25 #include "util/NDC.h"
26 #include "util/PathResolver.h"
27 #include "util/ReloadableXMLFile.h"
28 #include "util/Threads.h"
29 #include "util/XMLConstants.h"
30 #include "util/XMLHelper.h"
31
32 #if defined(XMLTOOLING_LOG4SHIB)
33 # include <log4shib/NDC.hh>
34 #elif defined(XMLTOOLING_LOG4CPP)
35 # include <log4cpp/NDC.hh>
36 #endif
37
38 #include <fstream>
39 #include <sys/types.h>
40 #include <sys/stat.h>
41
42 #include <xercesc/framework/LocalFileInputSource.hpp>
43 #include <xercesc/framework/Wrapper4InputSource.hpp>
44 #include <xercesc/util/XMLUniDefs.hpp>
45
46 using namespace xmltooling::logging;
47 using namespace xmltooling;
48 using namespace xercesc;
49 using namespace std;
50
51 static const XMLCh id[] =               UNICODE_LITERAL_2(i,d);
52 static const XMLCh uri[] =              UNICODE_LITERAL_3(u,r,i);
53 static const XMLCh url[] =              UNICODE_LITERAL_3(u,r,l);
54 static const XMLCh path[] =             UNICODE_LITERAL_4(p,a,t,h);
55 static const XMLCh pathname[] =         UNICODE_LITERAL_8(p,a,t,h,n,a,m,e);
56 static const XMLCh file[] =             UNICODE_LITERAL_4(f,i,l,e);
57 static const XMLCh filename[] =         UNICODE_LITERAL_8(f,i,l,e,n,a,m,e);
58 static const XMLCh validate[] =         UNICODE_LITERAL_8(v,a,l,i,d,a,t,e);
59 static const XMLCh reloadChanges[] =    UNICODE_LITERAL_13(r,e,l,o,a,d,C,h,a,n,g,e,s);
60 static const XMLCh reloadInterval[] =   UNICODE_LITERAL_14(r,e,l,o,a,d,I,n,t,e,r,v,a,l);
61 static const XMLCh backingFilePath[] =  UNICODE_LITERAL_15(b,a,c,k,i,n,g,F,i,l,e,P,a,t,h);
62
63
64 ReloadableXMLFile::ReloadableXMLFile(const DOMElement* e, Category& log)
65     : m_root(e), m_local(true), m_validate(false), m_backupIndicator(true), m_filestamp(0), m_reloadInterval(0), m_lock(NULL), m_log(log),
66         m_shutdown(false), m_reload_wait(NULL), m_reload_thread(NULL)
67 {
68 #ifdef _DEBUG
69     NDC ndc("ReloadableXMLFile");
70 #endif
71
72     // Establish source of data...
73     const XMLCh* source=e->getAttributeNS(NULL,uri);
74     if (!source || !*source) {
75         source=e->getAttributeNS(NULL,url);
76         if (!source || !*source) {
77             source=e->getAttributeNS(NULL,path);
78             if (!source || !*source) {
79                 source=e->getAttributeNS(NULL,pathname);
80                 if (!source || !*source) {
81                     source=e->getAttributeNS(NULL,file);
82                     if (!source || !*source) {
83                         source=e->getAttributeNS(NULL,filename);
84                     }
85                 }
86             }
87         }
88         else
89             m_local=false;
90     }
91     else
92         m_local=false;
93
94     if (source && *source) {
95         const XMLCh* flag=e->getAttributeNS(NULL,validate);
96         m_validate=(XMLString::equals(flag,xmlconstants::XML_TRUE) || XMLString::equals(flag,xmlconstants::XML_ONE));
97
98         auto_ptr_char temp(source);
99         m_source=temp.get();
100
101         if (!m_local && !strstr(m_source.c_str(),"://")) {
102             log.warn("deprecated usage of uri/url attribute for a local resource, use path instead");
103             m_local=true;
104         }
105
106         if (m_local) {
107             XMLToolingConfig::getConfig().getPathResolver()->resolve(m_source, PathResolver::XMLTOOLING_CFG_FILE);
108
109             flag=e->getAttributeNS(NULL,reloadChanges);
110             if (!XMLString::equals(flag,xmlconstants::XML_FALSE) && !XMLString::equals(flag,xmlconstants::XML_ZERO)) {
111 #ifdef WIN32
112                 struct _stat stat_buf;
113                 if (_stat(m_source.c_str(), &stat_buf) == 0)
114 #else
115                 struct stat stat_buf;
116                 if (stat(m_source.c_str(), &stat_buf) == 0)
117 #endif
118                     m_filestamp=stat_buf.st_mtime;
119                 else
120                     throw IOException("Unable to access local file ($1)", params(1,m_source.c_str()));
121                 m_lock=RWLock::create();
122             }
123             log.debug("using local resource (%s), will %smonitor for changes", m_source.c_str(), m_lock ? "" : "not ");
124         }
125         else {
126             log.debug("using remote resource (%s)", m_source.c_str());
127             source = e->getAttributeNS(NULL,backingFilePath);
128             if (source && *source) {
129                 auto_ptr_char temp2(source);
130                 m_backing=temp2.get();
131                 XMLToolingConfig::getConfig().getPathResolver()->resolve(m_backing, PathResolver::XMLTOOLING_RUN_FILE);
132                 log.debug("backup remote resource to (%s)", m_backing.c_str());
133             }
134             source = e->getAttributeNS(NULL,reloadInterval);
135             if (source && *source) {
136                 m_reloadInterval = XMLString::parseInt(source);
137                 if (m_reloadInterval > 0) {
138                     m_log.debug("will reload remote resource at most every %d seconds", m_reloadInterval);
139                     m_lock=RWLock::create();
140                 }
141             }
142             m_filestamp = time(NULL);   // assume it gets loaded initially
143         }
144
145         if (m_lock) {
146             m_reload_wait = CondWait::create();
147             m_reload_thread = Thread::create(&reload_fn, this);
148         }
149     }
150     else {
151         log.debug("no resource uri/path/name supplied, will load inline configuration");
152     }
153
154     source = e->getAttributeNS(NULL, id);
155     if (source && *source) {
156         auto_ptr_char tempid(source);
157         m_id = tempid.get();
158     }
159 }
160
161 ReloadableXMLFile::~ReloadableXMLFile()
162 {
163     shutdown();
164     delete m_lock;
165 }
166
167 void ReloadableXMLFile::shutdown()
168 {
169     if (m_reload_thread) {
170         // Shut down the reload thread and let it know.
171         m_shutdown = true;
172         m_reload_wait->signal();
173         m_reload_thread->join(NULL);
174         delete m_reload_thread;
175         delete m_reload_wait;
176         m_reload_thread = NULL;
177         m_reload_wait = NULL;
178     }
179 }
180
181 void* ReloadableXMLFile::reload_fn(void* pv)
182 {
183     ReloadableXMLFile* r = reinterpret_cast<ReloadableXMLFile*>(pv);
184
185 #ifndef WIN32
186     // First, let's block all signals
187     Thread::mask_all_signals();
188 #endif
189
190     if (!r->m_id.empty()) {
191         string threadid("[");
192         threadid += r->m_id + ']';
193         logging::NDC::push(threadid);
194     }
195
196 #ifdef _DEBUG
197     NDC ndc("reload");
198 #endif
199
200     auto_ptr<Mutex> mutex(Mutex::create());
201     mutex->lock();
202
203     if (r->m_local)
204         r->m_log.info("reload thread started...running when signaled");
205     else
206         r->m_log.info("reload thread started...running every %d seconds", r->m_reloadInterval);
207
208     while (!r->m_shutdown) {
209         if (r->m_local)
210             r->m_reload_wait->wait(mutex.get());
211         else
212             r->m_reload_wait->timedwait(mutex.get(), r->m_reloadInterval);
213         if (r->m_shutdown)
214             break;
215
216         try {
217             r->m_log.info("reloading %s resource...", r->m_local ? "local" : "remote");
218             pair<bool,DOMElement*> ret = r->background_load();
219             if (ret.first)
220                 ret.second->getOwnerDocument()->release();
221         }
222         catch (long& ex) {
223             if (ex == HTTPResponse::XMLTOOLING_HTTP_STATUS_NOTMODIFIED) {
224                 r->m_log.info("remote resource (%s) unchanged from cached version", r->m_source.c_str());
225             }
226             else {
227                 // Shouldn't happen, we should only get codes intended to be gracefully handled.
228                 r->m_log.crit("maintaining existing configuration, remote resource fetch returned atypical status code (%d)", ex);
229             }
230         }
231         catch (exception& ex) {
232             r->m_log.crit("maintaining existing configuration, error reloading resource (%s): %s", r->m_source.c_str(), ex.what());
233         }
234     }
235
236     r->m_log.info("reload thread finished");
237
238     mutex->unlock();
239
240     if (!r->m_id.empty()) {
241         logging::NDC::pop();
242     }
243
244     return NULL;
245 }
246
247 Lockable* ReloadableXMLFile::lock()
248 {
249     if (!m_lock)
250         return this;
251
252     m_lock->rdlock();
253
254     if (m_local) {
255     // Check if we need to refresh.
256 #ifdef WIN32
257         struct _stat stat_buf;
258         if (_stat(m_source.c_str(), &stat_buf) != 0)
259             return this;
260 #else
261         struct stat stat_buf;
262         if (stat(m_source.c_str(), &stat_buf) != 0)
263             return this;
264 #endif
265         if (m_filestamp >= stat_buf.st_mtime)
266             return this;
267
268         // Elevate lock and recheck.
269         m_log.debug("timestamp of local resource changed, elevating to a write lock");
270         m_lock->unlock();
271         m_lock->wrlock();
272         if (m_filestamp >= stat_buf.st_mtime) {
273             // Somebody else handled it, just downgrade.
274             m_log.debug("update of local resource handled by another thread, downgrading lock");
275             m_lock->unlock();
276             m_lock->rdlock();
277             return this;
278         }
279
280         // Update the timestamp regardless.
281         m_filestamp = stat_buf.st_mtime;
282         m_log.info("change detected, signaling reload thread...");
283         m_reload_wait->signal();
284     }
285
286     return this;
287 }
288
289 void ReloadableXMLFile::unlock()
290 {
291     if (m_lock)
292         m_lock->unlock();
293 }
294
295 pair<bool,DOMElement*> ReloadableXMLFile::load(bool backup)
296 {
297 #ifdef _DEBUG
298     NDC ndc("load");
299 #endif
300
301     try {
302         if (m_source.empty()) {
303             // Data comes from the DOM we were handed.
304             m_log.debug("loading inline configuration...");
305             return make_pair(false, XMLHelper::getFirstChildElement(m_root));
306         }
307         else {
308             // Data comes from a file we have to parse.
309             if (backup)
310                 m_log.warn("using local backup of remote resource");
311             else
312                 m_log.debug("loading configuration from external resource...");
313
314             DOMDocument* doc=NULL;
315             if (m_local || backup) {
316                 auto_ptr_XMLCh widenit(backup ? m_backing.c_str() : m_source.c_str());
317                 // Use library-wide lock for now, nothing else is using it anyway.
318                 Locker locker(backup ? getBackupLock() : NULL);
319                 LocalFileInputSource src(widenit.get());
320                 Wrapper4InputSource dsrc(&src, false);
321                 if (m_validate)
322                     doc=XMLToolingConfig::getConfig().getValidatingParser().parse(dsrc);
323                 else
324                     doc=XMLToolingConfig::getConfig().getParser().parse(dsrc);
325             }
326             else {
327                 URLInputSource src(m_root, NULL, &m_cacheTag);
328                 Wrapper4InputSource dsrc(&src, false);
329                 if (m_validate)
330                     doc=XMLToolingConfig::getConfig().getValidatingParser().parse(dsrc);
331                 else
332                     doc=XMLToolingConfig::getConfig().getParser().parse(dsrc);
333
334                 // Check for a response code signal.
335                 if (XMLHelper::isNodeNamed(doc->getDocumentElement(), xmlconstants::XMLTOOLING_NS, URLInputSource::utf16StatusCodeElementName)) {
336                     int responseCode = XMLString::parseInt(doc->getDocumentElement()->getFirstChild()->getNodeValue());
337                     doc->release();
338                     if (responseCode == HTTPResponse::XMLTOOLING_HTTP_STATUS_NOTMODIFIED) {
339                         throw (long)responseCode; // toss out as a "known" case to handle gracefully
340                     }
341                     else {
342                         m_log.warn("remote resource fetch returned atypical status code (%d)", responseCode);
343                         throw IOException("remote resource fetch failed, check log for status code of response");
344                     }
345                 }
346             }
347
348             m_log.infoStream() << "loaded XML resource (" << (backup ? m_backing : m_source) << ")" << logging::eol;
349
350             if (!backup && !m_backing.empty()) {
351                 // If the indicator is true, we're responsible for the backup.
352                 if (m_backupIndicator) {
353                     m_log.debug("backing up remote resource to (%s)", m_backing.c_str());
354                     try {
355                         Locker locker(getBackupLock());
356                         ofstream backer(m_backing.c_str());
357                         backer << *doc;
358                     }
359                     catch (exception& ex) {
360                         m_log.crit("exception while backing up resource: %s", ex.what());
361                     }
362                 }
363                 else {
364                     // If the indicator was false, set true to signal that a backup is needed.
365                     // The caller will presumably flip it back to false once that's done.
366                     m_backupIndicator = true;
367                 }
368             }
369
370             return make_pair(true, doc->getDocumentElement());
371         }
372     }
373     catch (XMLException& e) {
374         auto_ptr_char msg(e.getMessage());
375         m_log.errorStream() << "Xerces error while loading resource (" << (backup ? m_backing : m_source) << "): "
376             << msg.get() << logging::eol;
377         if (!backup && !m_backing.empty())
378             return load(true);
379         throw XMLParserException(msg.get());
380     }
381     catch (exception& e) {
382         m_log.errorStream() << "error while loading resource ("
383             << (m_source.empty() ? "inline" : (backup ? m_backing : m_source)) << "): " << e.what() << logging::eol;
384         if (!backup && !m_backing.empty())
385             return load(true);
386         throw;
387     }
388 }
389
390 pair<bool,DOMElement*> ReloadableXMLFile::load()
391 {
392     return load(false);
393 }
394
395 pair<bool,DOMElement*> ReloadableXMLFile::background_load()
396 {
397     // If this method isn't overridden, we acquire a write lock
398     // and just call the old override.
399     if (m_lock)
400         m_lock->wrlock();
401     SharedLock locker(m_lock, false);
402     return load();
403 }
404
405 Lockable* ReloadableXMLFile::getBackupLock()
406 {
407     return &XMLToolingConfig::getConfig();
408 }