001/*
002 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     bstefanescu
018 */
019package org.nuxeo.runtime.reload;
020
021import java.io.File;
022import java.io.IOException;
023import java.net.MalformedURLException;
024import java.net.URL;
025import java.time.Duration;
026import java.util.Collections;
027import java.util.jar.Manifest;
028
029import javax.transaction.Transaction;
030
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033import org.nuxeo.common.Environment;
034import org.nuxeo.common.utils.FileUtils;
035import org.nuxeo.common.utils.JarUtils;
036import org.nuxeo.common.utils.ZipUtils;
037import org.nuxeo.runtime.RuntimeService;
038import org.nuxeo.runtime.RuntimeServiceException;
039import org.nuxeo.runtime.api.Framework;
040import org.nuxeo.runtime.api.ServicePassivator;
041import org.nuxeo.runtime.deployment.preprocessor.DeploymentPreprocessor;
042import org.nuxeo.runtime.model.ComponentContext;
043import org.nuxeo.runtime.model.DefaultComponent;
044import org.nuxeo.runtime.services.event.Event;
045import org.nuxeo.runtime.services.event.EventService;
046import org.nuxeo.runtime.transaction.TransactionHelper;
047import org.osgi.framework.Bundle;
048import org.osgi.framework.BundleContext;
049import org.osgi.framework.BundleException;
050import org.osgi.framework.ServiceReference;
051import org.osgi.service.packageadmin.PackageAdmin;
052
053/**
054 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
055 */
056public class ReloadComponent extends DefaultComponent implements ReloadService {
057
058    private static final Log log = LogFactory.getLog(ReloadComponent.class);
059
060    protected static Bundle bundle;
061
062    protected Long lastFlushed;
063
064    public static BundleContext getBundleContext() {
065        return bundle.getBundleContext();
066    }
067
068    public static Bundle getBundle() {
069        return bundle;
070    }
071
072    @Override
073    public void activate(ComponentContext context) {
074        super.activate(context);
075        bundle = context.getRuntimeContext().getBundle();
076    }
077
078    @Override
079    public void deactivate(ComponentContext context) {
080        super.deactivate(context);
081        bundle = null;
082    }
083
084    @Override
085    public void reload() {
086        if (log.isDebugEnabled()) {
087            log.debug("Starting reload");
088        }
089        try {
090            reloadProperties();
091        } catch (IOException e) {
092            throw new RuntimeServiceException(e);
093        }
094        triggerReloadWithNewTransaction(RELOAD_EVENT_ID);
095    }
096
097    @Override
098    public void reloadProperties() throws IOException {
099        log.info("Reload runtime properties");
100        Framework.getRuntime().reloadProperties();
101    }
102
103    @Override
104    public void reloadRepository() {
105        log.info("Reload repository");
106        triggerReloadWithNewTransaction(RELOAD_REPOSITORIES_ID);
107    }
108
109    @Override
110    public void reloadSeamComponents() {
111        log.info("Reload Seam components");
112        Framework.getLocalService(EventService.class).sendEvent(
113                new Event(RELOAD_TOPIC, RELOAD_SEAM_EVENT_ID, this, null));
114    }
115
116    @Override
117    public void flush() {
118        log.info("Flush caches");
119        Framework.getLocalService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, FLUSH_EVENT_ID, this, null));
120        flushJaasCache();
121        setFlushedNow();
122    }
123
124    @Override
125    public void flushJaasCache() {
126        log.info("Flush the JAAS cache");
127        Framework.getLocalService(EventService.class).sendEvent(
128                new Event("usermanager", "user_changed", this, "Deployer"));
129        setFlushedNow();
130    }
131
132    @Override
133    public void flushSeamComponents() {
134        log.info("Flush Seam components");
135        Framework.getLocalService(EventService.class).sendEvent(
136                new Event(RELOAD_TOPIC, FLUSH_SEAM_EVENT_ID, this, null));
137        setFlushedNow();
138    }
139
140    @Override
141    public String deployBundle(File file) throws BundleException {
142        return deployBundle(file, false);
143    }
144
145    @Override
146    public String deployBundle(File file, boolean reloadResourceClasspath) throws BundleException {
147        String name = getOSGIBundleName(file);
148        if (name == null) {
149            log.error(String.format("No Bundle-SymbolicName found in MANIFEST for jar at '%s'", file.getAbsolutePath()));
150            return null;
151        }
152
153        String path = file.getAbsolutePath();
154
155        log.info(String.format("Before deploy bundle for file at '%s'\n" + "%s", path, getRuntimeStatus()));
156
157        if (reloadResourceClasspath) {
158            URL url;
159            try {
160                url = new File(path).toURI().toURL();
161            } catch (MalformedURLException e) {
162                throw new RuntimeException(e);
163            }
164            Framework.reloadResourceLoader(Collections.singletonList(url), null);
165        }
166
167        // check if this is a bundle first
168        Bundle newBundle = getBundleContext().installBundle(path);
169        if (newBundle == null) {
170            throw new IllegalArgumentException("Could not find a valid bundle at path: " + path);
171        }
172        Transaction tx = TransactionHelper.suspendTransaction();
173        try {
174            newBundle.start();
175        } finally {
176            TransactionHelper.resumeTransaction(tx);
177        }
178
179        log.info(String.format("Deploy done for bundle with name '%s'.\n" + "%s", newBundle.getSymbolicName(),
180                getRuntimeStatus()));
181
182        return newBundle.getSymbolicName();
183    }
184
185    @Override
186    public void undeployBundle(File file, boolean reloadResources) throws BundleException {
187        String name = getOSGIBundleName(file);
188        String path = file.getAbsolutePath();
189        if (name == null) {
190            log.error(String.format("No Bundle-SymbolicName found in MANIFEST for jar at '%s'", path));
191            return;
192        }
193
194        undeployBundle(name);
195
196        if (reloadResources) {
197            URL url;
198            try {
199                url = new File(path).toURI().toURL();
200            } catch (MalformedURLException e) {
201                throw new RuntimeException(e);
202            }
203            Framework.reloadResourceLoader(null, Collections.singletonList(url));
204        }
205    }
206
207    @Override
208    public void undeployBundle(String bundleName) throws BundleException {
209        if (bundleName == null) {
210            // ignore
211            return;
212        }
213        log.info(String.format("Before undeploy bundle with name '%s'.\n" + "%s", bundleName, getRuntimeStatus()));
214        BundleContext ctx = getBundleContext();
215        ServiceReference ref = ctx.getServiceReference(PackageAdmin.class.getName());
216        PackageAdmin srv = (PackageAdmin) ctx.getService(ref);
217        try {
218            for (Bundle b : srv.getBundles(bundleName, null)) {
219                if (b != null && b.getState() == Bundle.ACTIVE) {
220                    Transaction tx = TransactionHelper.suspendTransaction();
221                    try {
222                        b.stop();
223                        b.uninstall();
224                    } finally {
225                        TransactionHelper.resumeTransaction(tx);
226                    }
227                }
228            }
229        } finally {
230            ctx.ungetService(ref);
231        }
232        log.info(String.format("Undeploy done.\n" + "%s", getRuntimeStatus()));
233    }
234
235    @Override
236    public Long lastFlushed() {
237        return lastFlushed;
238    }
239
240    /**
241     * Sets the last date date to current date timestamp
242     *
243     * @since 5.6
244     */
245    protected void setFlushedNow() {
246        lastFlushed = System.currentTimeMillis();
247    }
248
249    @Override
250    public void runDeploymentPreprocessor() throws IOException {
251        if (log.isDebugEnabled()) {
252            log.debug("Start running deployment preprocessor");
253        }
254        String rootPath = Environment.getDefault().getRuntimeHome().getAbsolutePath();
255        File root = new File(rootPath);
256        DeploymentPreprocessor processor = new DeploymentPreprocessor(root);
257        // initialize
258        processor.init();
259        // and predeploy
260        processor.predeploy();
261        if (log.isDebugEnabled()) {
262            log.debug("Deployment preprocessing done");
263        }
264    }
265
266    protected static File getAppDir() {
267        return Environment.getDefault().getConfig().getParentFile();
268    }
269
270    protected static File getWarDir() {
271        return new File(getAppDir(), "nuxeo.war");
272    }
273
274    @Override
275    public String getOSGIBundleName(File file) {
276        Manifest mf = JarUtils.getManifest(file);
277        if (mf == null) {
278            return null;
279        }
280        String bundleName = mf.getMainAttributes().getValue("Bundle-SymbolicName");
281        if (bundleName == null) {
282            return null;
283        }
284        int index = bundleName.indexOf(';');
285        if (index > -1) {
286            bundleName = bundleName.substring(0, index);
287        }
288        return bundleName;
289    }
290
291    protected String getRuntimeStatus() {
292        StringBuilder msg = new StringBuilder();
293        RuntimeService runtime = Framework.getRuntime();
294        runtime.getStatusMessage(msg);
295        return msg.toString();
296    }
297
298    protected void triggerReloadWithNewTransaction(String id) {
299        if (TransactionHelper.isTransactionMarkedRollback()) {
300            throw new AssertionError("The calling transaction is marked rollback");
301        }
302        // we need to commit or rollback transaction because suspending it leads to a lock/errors when acquiring a new
303        // connection during the datasource reload
304        TransactionHelper.commitOrRollbackTransaction();
305        TransactionHelper.startTransaction();
306        try {
307            try {
308                triggerReloadWithPassivate(id);
309            } catch (RuntimeException cause) {
310                TransactionHelper.setTransactionRollbackOnly();
311                throw cause;
312            } finally {
313                TransactionHelper.commitOrRollbackTransaction();
314            }
315        } finally {
316            TransactionHelper.startTransaction();
317        }
318    }
319
320    protected void triggerReloadWithPassivate(String id) {
321        log.info("about to passivate for " + id);
322        Framework.getLocalService(EventService.class).sendEvent(
323                new Event(RELOAD_TOPIC, BEFORE_RELOAD_EVENT_ID, this, null));
324        try {
325            ServicePassivator.proceed(Duration.ofSeconds(5), Duration.ofSeconds(30), true, () -> {
326                log.info("about to send " + id);
327                Framework.getLocalService(EventService.class).sendEvent(new Event(RELOAD_TOPIC, id, this, null));
328            }).onFailure(
329                    snapshot -> {
330                        throw new UnsupportedOperationException("Detected access, should initiate a reboot "
331                                + snapshot.toString());
332                    });
333        } finally {
334            Framework.getLocalService(EventService.class).sendEvent(
335                    new Event(RELOAD_TOPIC, AFTER_RELOAD_EVENT_ID, this, null));
336            log.info("returning from " + id);
337        }
338    }
339}