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