]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/internal/kernel/Kernel.java
Improve logging and warnings.
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / internal / kernel / Kernel.java
1 package org.argeo.cms.internal.kernel;
2
3 import static bitronix.tm.TransactionManagerServices.getTransactionManager;
4 import static bitronix.tm.TransactionManagerServices.getTransactionSynchronizationRegistry;
5 import static java.util.Locale.ENGLISH;
6 import static org.argeo.cms.internal.kernel.KernelUtils.getFrameworkProp;
7 import static org.argeo.cms.internal.kernel.KernelUtils.getOsgiInstanceDir;
8 import static org.argeo.jcr.ArgeoJcrConstants.ALIAS_NODE;
9 import static org.argeo.jcr.ArgeoJcrConstants.JCR_REPOSITORY_ALIAS;
10 import static org.argeo.util.LocaleChoice.asLocaleList;
11 import static org.osgi.framework.Constants.FRAMEWORK_UUID;
12
13 import java.io.ByteArrayInputStream;
14 import java.io.ByteArrayOutputStream;
15 import java.io.File;
16 import java.io.FileFilter;
17 import java.io.IOException;
18 import java.lang.management.ManagementFactory;
19 import java.security.PrivilegedAction;
20 import java.util.HashMap;
21 import java.util.Hashtable;
22 import java.util.List;
23 import java.util.Locale;
24 import java.util.Map;
25
26 import javax.jcr.ImportUUIDBehavior;
27 import javax.jcr.Repository;
28 import javax.jcr.RepositoryFactory;
29 import javax.jcr.Session;
30 import javax.jcr.SimpleCredentials;
31 import javax.security.auth.Subject;
32 import javax.transaction.TransactionManager;
33 import javax.transaction.TransactionSynchronizationRegistry;
34 import javax.transaction.UserTransaction;
35
36 import org.apache.commons.io.FileUtils;
37 import org.apache.commons.logging.Log;
38 import org.apache.commons.logging.LogFactory;
39 import org.apache.jackrabbit.api.JackrabbitRepository;
40 import org.apache.jackrabbit.util.TransientFileFactory;
41 import org.argeo.ArgeoException;
42 import org.argeo.ArgeoLogger;
43 import org.argeo.cms.CmsException;
44 import org.argeo.cms.maintenance.MaintenanceUi;
45 import org.argeo.jackrabbit.OsgiJackrabbitRepositoryFactory;
46 import org.argeo.jcr.ArgeoJcrConstants;
47 import org.argeo.jcr.ArgeoJcrUtils;
48 import org.eclipse.equinox.http.jetty.JettyConfigurator;
49 import org.eclipse.equinox.http.jetty.JettyConstants;
50 import org.eclipse.equinox.http.servlet.ExtendedHttpService;
51 import org.eclipse.rap.rwt.application.ApplicationConfiguration;
52 import org.osgi.framework.BundleContext;
53 import org.osgi.framework.ServiceEvent;
54 import org.osgi.framework.ServiceListener;
55 import org.osgi.framework.ServiceReference;
56 import org.osgi.framework.ServiceRegistration;
57 import org.osgi.framework.startlevel.BundleStartLevel;
58 import org.osgi.service.cm.Configuration;
59 import org.osgi.service.cm.ConfigurationAdmin;
60 import org.osgi.service.log.LogReaderService;
61 import org.osgi.service.useradmin.UserAdmin;
62 import org.osgi.util.tracker.ServiceTracker;
63
64 import bitronix.tm.BitronixTransactionManager;
65 import bitronix.tm.BitronixTransactionSynchronizationRegistry;
66 import bitronix.tm.TransactionManagerServices;
67
68 /**
69 * Argeo CMS Kernel. Responsible for :
70 * <ul>
71 * <li>security</li>
72 * <li>provisioning</li>
73 * <li>transaction</li>
74 * <li>logging</li>
75 * <li>local and remote file systems access</li>
76 * <li>OS access</li>
77 * </ul>
78 */
79 final class Kernel implements KernelHeader, KernelConstants, ServiceListener {
80 /*
81 * SERVICE REFERENCES
82 */
83 private ServiceReference<ConfigurationAdmin> configurationAdmin;
84 /*
85 * REGISTERED SERVICES
86 */
87 private ServiceRegistration<ArgeoLogger> loggerReg;
88 private ServiceRegistration<TransactionManager> tmReg;
89 private ServiceRegistration<UserTransaction> utReg;
90 private ServiceRegistration<TransactionSynchronizationRegistry> tsrReg;
91 private ServiceRegistration<? extends Repository> repositoryReg;
92 private ServiceRegistration<RepositoryFactory> repositoryFactoryReg;
93 private ServiceRegistration<UserAdmin> userAdminReg;
94
95 /*
96 * SERVICES IMPLEMENTATIONS
97 */
98 private NodeLogger logger;
99 private BitronixTransactionManager transactionManager;
100 private BitronixTransactionSynchronizationRegistry transactionSynchronizationRegistry;
101 private OsgiJackrabbitRepositoryFactory repositoryFactory;
102 NodeRepository repository;
103 private NodeUserAdmin userAdmin;
104
105 // Members
106 private final static Log log = LogFactory.getLog(Kernel.class);
107 ThreadGroup threadGroup = new ThreadGroup(Kernel.class.getSimpleName());
108 private final BundleContext bc = Activator.getBundleContext();
109 private final NodeSecurity nodeSecurity;
110 private DataHttp dataHttp;
111 private NodeHttp nodeHttp;
112 private KernelThread kernelThread;
113
114 private Locale defaultLocale = null;
115 private List<Locale> locales = null;
116
117 public Kernel() {
118 // KernelUtils.logFrameworkProperties(log);
119 nodeSecurity = new NodeSecurity();
120 }
121
122 final void init() {
123 Subject.doAs(nodeSecurity.getKernelSubject(),
124 new PrivilegedAction<Void>() {
125 @Override
126 public Void run() {
127 doInit();
128 return null;
129 }
130 });
131 }
132
133 private void doInit() {
134 long begin = System.currentTimeMillis();
135 // Use CMS bundle classloader
136 ClassLoader currentContextCl = Thread.currentThread()
137 .getContextClassLoader();
138 Thread.currentThread().setContextClassLoader(
139 Kernel.class.getClassLoader());
140 try {
141 if (nodeSecurity.isFirstInit())
142 firstInit();
143
144 defaultLocale = new Locale(getFrameworkProp(I18N_DEFAULT_LOCALE,
145 ENGLISH.getLanguage()));
146 locales = asLocaleList(getFrameworkProp(I18N_LOCALES));
147
148 ServiceTracker<LogReaderService, LogReaderService> logReaderService = new ServiceTracker<LogReaderService, LogReaderService>(
149 bc, LogReaderService.class, null);
150 logReaderService.open();
151 logger = new NodeLogger(logReaderService.getService());
152 logReaderService.close();
153
154 if (isMaintenance())
155 maintenanceInit();
156 else
157 normalInit();
158 } catch (Exception e) {
159 log.error("Cannot initialize Argeo CMS", e);
160 throw new ArgeoException("Cannot initialize", e);
161 } finally {
162 Thread.currentThread().setContextClassLoader(currentContextCl);
163 }
164
165 long jvmUptime = ManagementFactory.getRuntimeMXBean().getUptime();
166 log.info("## ARGEO CMS UP in " + (jvmUptime / 1000) + "."
167 + (jvmUptime % 1000) + "s ##");
168 long initDuration = System.currentTimeMillis() - begin;
169 if (log.isTraceEnabled())
170 log.trace("Kernel initialization took " + initDuration + "ms");
171 directorsCut(initDuration);
172 }
173
174 private void normalInit() {
175 ConfigurationAdmin conf = findConfigurationAdmin();
176 // Initialise services
177 initTransactionManager();
178 if (repository == null)
179 repository = new NodeRepository();
180 if (repositoryFactory == null)
181 repositoryFactory = new OsgiJackrabbitRepositoryFactory();
182 userAdmin = new NodeUserAdmin(transactionManager, repository);
183
184 // HTTP
185 initWebServer(conf);
186 ServiceReference<ExtendedHttpService> sr = bc
187 .getServiceReference(ExtendedHttpService.class);
188 if (sr != null)
189 addHttpService(sr);
190
191 // ADMIN UIs
192 UserUi userUi = new UserUi();
193 Hashtable<String, String> props = new Hashtable<String, String>();
194 props.put("contextName", "user");
195 bc.registerService(ApplicationConfiguration.class, userUi, props);
196
197 // Kernel thread
198 kernelThread = new KernelThread(this);
199 kernelThread.setContextClassLoader(Kernel.class.getClassLoader());
200 kernelThread.start();
201
202 // Publish services to OSGi
203 publish();
204 }
205
206 private boolean isMaintenance() {
207 String startLevel = KernelUtils.getFrameworkProp("osgi.startLevel");
208 if (startLevel == null)
209 return false;
210 int bundleStartLevel = bc.getBundle().adapt(BundleStartLevel.class)
211 .getStartLevel();
212 // int frameworkStartLevel =
213 // bc.getBundle(0).adapt(BundleStartLevel.class)
214 // .getStartLevel();
215 int frameworkStartLevel = Integer.parseInt(startLevel);
216 // int frameworkStartLevel = bc.getBundle(0)
217 // .adapt(FrameworkStartLevel.class).getStartLevel();
218 return bundleStartLevel == frameworkStartLevel;
219 }
220
221 private void maintenanceInit() {
222 log.info("## MAINTENANCE ##");
223 bc.addServiceListener(Kernel.this);
224 initWebServer(null);
225 MaintenanceUi maintenanceUi = new MaintenanceUi();
226 Hashtable<String, String> props = new Hashtable<String, String>();
227 props.put("contextName", "maintenance");
228 bc.registerService(ApplicationConfiguration.class, maintenanceUi, props);
229 }
230
231 private void firstInit() {
232 log.info("## FIRST INIT ##");
233 String nodeInit = getFrameworkProp(NODE_INIT);
234 if (nodeInit == null)
235 nodeInit = "../../init";
236 if (nodeInit.startsWith("http")) {
237 remoteFirstInit(nodeInit);
238 return;
239 }
240 File initDir;
241 if (nodeInit.startsWith("."))
242 initDir = KernelUtils.getExecutionDir(nodeInit);
243 else
244 initDir = new File(nodeInit);
245 // TODO also uncompress archives
246 if (initDir.exists())
247 try {
248 FileUtils.copyDirectory(initDir, getOsgiInstanceDir(),
249 new FileFilter() {
250
251 @Override
252 public boolean accept(File pathname) {
253 if (pathname.getName().equals(".svn")
254 || pathname.getName().equals(".git"))
255 return false;
256 return true;
257 }
258 });
259 log.info("CMS initialized from " + initDir.getCanonicalPath());
260 } catch (IOException e) {
261 throw new CmsException("Cannot initialize from " + initDir, e);
262 }
263 }
264
265 private void remoteFirstInit(String uri) {
266 try {
267 repository = new NodeRepository();
268 repositoryFactory = new OsgiJackrabbitRepositoryFactory();
269 Repository remoteRepository = ArgeoJcrUtils.getRepositoryByUri(
270 repositoryFactory, uri);
271 Session remoteSession = remoteRepository
272 .login(new SimpleCredentials("root", "demo".toCharArray()),
273 "main");
274 Session localSession = this.repository.login();
275 // FIXME register node type
276 // if (false)
277 // CndImporter.registerNodeTypes(null, localSession);
278 ByteArrayOutputStream out = new ByteArrayOutputStream();
279 remoteSession.exportSystemView("/", out, true, false);
280 ByteArrayInputStream in = new ByteArrayInputStream(
281 out.toByteArray());
282 localSession.importXML("/", in,
283 ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW);
284 // JcrUtils.copy(remoteSession.getRootNode(),
285 // localSession.getRootNode());
286 } catch (Exception e) {
287 throw new CmsException("Cannot first init from " + uri, e);
288 }
289 }
290
291 /** Can be null */
292 private ConfigurationAdmin findConfigurationAdmin() {
293 configurationAdmin = bc.getServiceReference(ConfigurationAdmin.class);
294 if (configurationAdmin == null) {
295 return null;
296 }
297 return bc.getService(configurationAdmin);
298 }
299
300 private void initTransactionManager() {
301 bitronix.tm.Configuration tmConf = TransactionManagerServices
302 .getConfiguration();
303 tmConf.setServerId(getFrameworkProp(FRAMEWORK_UUID));
304
305 // File tmBaseDir = new File(getFrameworkProp(TRANSACTIONS_HOME,
306 // getOsgiInstancePath(DIR_TRANSACTIONS)));
307 File tmBaseDir = bc.getDataFile(DIR_TRANSACTIONS);
308 File tmDir1 = new File(tmBaseDir, "btm1");
309 tmDir1.mkdirs();
310 tmConf.setLogPart1Filename(new File(tmDir1, tmDir1.getName() + ".tlog")
311 .getAbsolutePath());
312 File tmDir2 = new File(tmBaseDir, "btm2");
313 tmDir2.mkdirs();
314 tmConf.setLogPart2Filename(new File(tmDir2, tmDir2.getName() + ".tlog")
315 .getAbsolutePath());
316 transactionManager = getTransactionManager();
317 transactionSynchronizationRegistry = getTransactionSynchronizationRegistry();
318 }
319
320 private void initWebServer(ConfigurationAdmin conf) {
321 String httpPort = getFrameworkProp("org.osgi.service.http.port");
322 String httpsPort = getFrameworkProp("org.osgi.service.http.port.secure");
323 try {
324 if (httpPort != null || httpsPort != null) {
325 Hashtable<String, Object> jettyProps = new Hashtable<String, Object>();
326 if (httpPort != null) {
327 jettyProps.put(JettyConstants.HTTP_PORT, httpPort);
328 jettyProps.put(JettyConstants.HTTP_ENABLED, true);
329 }
330 if (httpsPort != null) {
331 jettyProps.put(JettyConstants.HTTPS_PORT, httpsPort);
332 jettyProps.put(JettyConstants.HTTPS_ENABLED, true);
333 jettyProps.put(JettyConstants.SSL_KEYSTORETYPE, "PKCS12");
334 jettyProps.put(JettyConstants.SSL_KEYSTORE, nodeSecurity
335 .getHttpServerKeyStore().getCanonicalPath());
336 jettyProps.put(JettyConstants.SSL_PASSWORD, "changeit");
337 jettyProps.put(JettyConstants.SSL_WANTCLIENTAUTH, true);
338 }
339 if (conf != null) {
340 // TODO make filter more generic
341 String filter = "(" + JettyConstants.HTTP_PORT + "="
342 + httpPort + ")";
343 if (conf.listConfigurations(filter) != null)
344 return;
345 Configuration jettyConf = conf.createFactoryConfiguration(
346 JETTY_FACTORY_PID, null);
347 jettyConf.update(jettyProps);
348 } else {
349 JettyConfigurator.startServer("default", jettyProps);
350 }
351 }
352 } catch (Exception e) {
353 throw new CmsException("Cannot initialize web server on "
354 + httpPortsMsg(httpPort, httpsPort), e);
355 }
356 }
357
358 @SuppressWarnings("unchecked")
359 private void publish() {
360 // Listen to service publication (also ours)
361 bc.addServiceListener(Kernel.this);
362
363 // Logging
364 loggerReg = bc.registerService(ArgeoLogger.class, logger, null);
365 // Transaction
366 tmReg = bc.registerService(TransactionManager.class,
367 transactionManager, null);
368 utReg = bc.registerService(UserTransaction.class, transactionManager,
369 null);
370 tsrReg = bc.registerService(TransactionSynchronizationRegistry.class,
371 transactionSynchronizationRegistry, null);
372 // User admin
373 userAdminReg = bc.registerService(UserAdmin.class, userAdmin,
374 userAdmin.currentState());
375 // JCR
376 Hashtable<String, String> regProps = new Hashtable<String, String>();
377 regProps.put(JCR_REPOSITORY_ALIAS, ALIAS_NODE);
378 repositoryReg = (ServiceRegistration<? extends Repository>) bc
379 .registerService(new String[] { Repository.class.getName(),
380 JackrabbitRepository.class.getName() }, repository,
381 regProps);
382 repositoryFactoryReg = bc.registerService(RepositoryFactory.class,
383 repositoryFactory, null);
384 }
385
386 void destroy() {
387 long begin = System.currentTimeMillis();
388 unpublish();
389
390 kernelThread.destroyAndJoin();
391
392 if (dataHttp != null)
393 dataHttp.destroy();
394 if (nodeHttp != null)
395 nodeHttp.destroy();
396 if (userAdmin != null)
397 userAdmin.destroy();
398 if (repository != null)
399 repository.destroy();
400 if (transactionManager != null)
401 transactionManager.shutdown();
402
403 bc.removeServiceListener(this);
404
405 // Clean hanging threads from Jackrabbit
406 TransientFileFactory.shutdown();
407
408 // Clean hanging Gogo shell thread
409 new GogoShellKiller().start();
410
411 nodeSecurity.destroy();
412 long duration = System.currentTimeMillis() - begin;
413 log.info("## ARGEO CMS DOWN in " + (duration / 1000) + "."
414 + (duration % 1000) + "s ##");
415 }
416
417 private void unpublish() {
418 userAdminReg.unregister();
419 repositoryFactoryReg.unregister();
420 repositoryReg.unregister();
421 tmReg.unregister();
422 utReg.unregister();
423 tsrReg.unregister();
424 loggerReg.unregister();
425 }
426
427 @Override
428 public void serviceChanged(ServiceEvent event) {
429 ServiceReference<?> sr = event.getServiceReference();
430 Object service = bc.getService(sr);
431 if (service instanceof Repository) {
432 Object jcrRepoAlias = sr
433 .getProperty(ArgeoJcrConstants.JCR_REPOSITORY_ALIAS);
434 if (jcrRepoAlias != null) {// JCR repository
435 String alias = jcrRepoAlias.toString();
436 Repository repository = (Repository) bc.getService(sr);
437 Map<String, Object> props = new HashMap<String, Object>();
438 for (String key : sr.getPropertyKeys())
439 props.put(key, sr.getProperty(key));
440 if (ServiceEvent.REGISTERED == event.getType()) {
441 try {
442 repositoryFactory.register(repository, props);
443 dataHttp.registerRepositoryServlets(alias, repository);
444 } catch (Exception e) {
445 throw new CmsException(
446 "Could not publish JCR repository " + alias, e);
447 }
448 } else if (ServiceEvent.UNREGISTERING == event.getType()) {
449 repositoryFactory.unregister(repository, props);
450 dataHttp.unregisterRepositoryServlets(alias);
451 }
452 }
453 } else if (service instanceof ExtendedHttpService) {
454 if (ServiceEvent.REGISTERED == event.getType()) {
455 addHttpService(sr);
456 } else if (ServiceEvent.UNREGISTERING == event.getType()) {
457 dataHttp.destroy();
458 dataHttp = null;
459 }
460 }
461 }
462
463 private void addHttpService(ServiceReference<?> sr) {
464 // for (String key : sr.getPropertyKeys())
465 // log.debug(key + "=" + sr.getProperty(key));
466 ExtendedHttpService httpService = (ExtendedHttpService) bc
467 .getService(sr);
468 // TODO find constants
469 Object httpPort = sr.getProperty("http.port");
470 Object httpsPort = sr.getProperty("https.port");
471 dataHttp = new DataHttp(httpService, repository);
472 nodeHttp = new NodeHttp(httpService);
473 if (log.isDebugEnabled())
474 log.debug(httpPortsMsg(httpPort, httpsPort));
475 }
476
477 private String httpPortsMsg(Object httpPort, Object httpsPort) {
478 return "HTTP " + httpPort
479 + (httpsPort != null ? " - HTTPS " + httpsPort : "");
480 }
481
482 @Override
483 public Locale getDefaultLocale() {
484 return defaultLocale;
485 }
486
487 /** Can be null. */
488 @Override
489 public List<Locale> getLocales() {
490 return locales;
491 }
492
493 final private static void directorsCut(long initDuration) {
494 // final long ms = 128l + (long) (Math.random() * 128d);
495 long ms = initDuration / 100;
496 log.info("Spend " + ms + "ms"
497 + " reflecting on the progress brought to mankind"
498 + " by Free Software...");
499 long beginNano = System.nanoTime();
500 try {
501 Thread.sleep(ms, 0);
502 } catch (InterruptedException e) {
503 // silent
504 }
505 long durationNano = System.nanoTime() - beginNano;
506 final double M = 1000d * 1000d;
507 double sleepAccuracy = ((double) durationNano) / (ms * M);
508 if (log.isDebugEnabled())
509 log.debug("Sleep accuracy: "
510 + String.format("%.2f", 100 - (sleepAccuracy * 100 - 100))
511 + " %");
512 }
513
514 /** Workaround for blocking Gogo shell by system shutdown. */
515 private class GogoShellKiller extends Thread {
516
517 public GogoShellKiller() {
518 super("Gogo shell killer");
519 setDaemon(true);
520 }
521
522 @Override
523 public void run() {
524 ThreadGroup rootTg = getRootThreadGroup(null);
525 Thread gogoShellThread = findGogoShellThread(rootTg);
526 if (gogoShellThread == null)
527 return;
528 while (getNonDaemonCount(rootTg) > 2) {
529 try {
530 Thread.sleep(100);
531 } catch (InterruptedException e) {
532 // silent
533 }
534 }
535 gogoShellThread = findGogoShellThread(rootTg);
536 if (gogoShellThread == null)
537 return;
538 System.exit(0);
539 }
540 }
541
542 private static ThreadGroup getRootThreadGroup(ThreadGroup tg) {
543 if (tg == null)
544 tg = Thread.currentThread().getThreadGroup();
545 if (tg.getParent() == null)
546 return tg;
547 else
548 return getRootThreadGroup(tg.getParent());
549 }
550
551 private static int getNonDaemonCount(ThreadGroup rootThreadGroup) {
552 Thread[] threads = new Thread[rootThreadGroup.activeCount()];
553 rootThreadGroup.enumerate(threads);
554 int nonDameonCount = 0;
555 for (Thread t : threads)
556 if (!t.isDaemon())
557 nonDameonCount++;
558 return nonDameonCount;
559 }
560
561 private static Thread findGogoShellThread(ThreadGroup rootThreadGroup) {
562 Thread[] threads = new Thread[rootThreadGroup.activeCount()];
563 rootThreadGroup.enumerate(threads, true);
564 for (Thread thread : threads) {
565 if (thread.getName().equals("Gogo shell"))
566 return thread;
567 }
568 return null;
569 }
570
571 }