/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.ignite.internal.processors.cluster; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLConnection; import java.util.Collection; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicReference; import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteLogger; import org.apache.ignite.IgniteSystemProperties; import org.apache.ignite.internal.GridKernalGateway; import org.apache.ignite.internal.IgniteKernal; import org.apache.ignite.internal.IgniteProperties; import org.apache.ignite.internal.util.typedef.F; import org.apache.ignite.internal.util.typedef.internal.SB; import org.apache.ignite.internal.util.typedef.internal.U; import org.apache.ignite.internal.util.worker.GridWorker; import org.apache.ignite.plugin.PluginProvider; import org.jetbrains.annotations.Nullable; import static java.net.URLEncoder.encode; /** * This class is responsible for notification about new version availability. * <p> * Note also that this connectivity is not necessary to successfully start the system as it will * gracefully ignore any errors occurred during notification and verification process. */ class GridUpdateNotifier { /** Default encoding. */ private static final String CHARSET = "UTF-8"; /** Access URL to be used to access latest version data. */ private final String UPD_STATUS_PARAMS = IgniteProperties.get("ignite.update.status.params"); /** Throttling for logging out. */ private static final long THROTTLE_PERIOD = 24 * 60 * 60 * 1000; // 1 day. /** Sleep milliseconds time for worker thread. */ private static final int WORKER_THREAD_SLEEP_TIME = 5000; /** Url for request version. */ private final static String UPDATE_NOTIFIER_URL = "https://ignite.run/update_status_ignite-plain-text.php"; /** Grid version. */ private final String ver; /** Latest version. */ private volatile String latestVer; /** Download url for latest version. */ private volatile String downloadUrl; /** Ignite instance name. */ private final String igniteInstanceName; /** Whether or not to report only new version. */ private volatile boolean reportOnlyNew; /** */ private volatile int topSize; /** System properties */ private final String vmProps; /** Plugins information for request */ private final String pluginsVers; /** Kernal gateway */ private final GridKernalGateway gw; /** */ private long lastLog = -1; /** Command for worker thread. */ private final AtomicReference<Runnable> cmd = new AtomicReference<>(); /** Worker thread to process http request. */ private final Thread workerThread; /** * Creates new notifier with default values. * * @param igniteInstanceName igniteInstanceName * @param ver Compound Ignite version. * @param gw Kernal gateway. * @param pluginProviders Kernal gateway. * @param reportOnlyNew Whether or not to report only new version. * @throws IgniteCheckedException If failed. */ GridUpdateNotifier(String igniteInstanceName, String ver, GridKernalGateway gw, Collection<PluginProvider> pluginProviders, boolean reportOnlyNew) throws IgniteCheckedException { try { this.ver = ver; this.igniteInstanceName = igniteInstanceName == null ? "null" : igniteInstanceName; this.gw = gw; SB pluginsBuilder = new SB(); for (PluginProvider provider : pluginProviders) pluginsBuilder.a("&").a(provider.name() + "-plugin-version").a("="). a(encode(provider.version(), CHARSET)); pluginsVers = pluginsBuilder.toString(); this.reportOnlyNew = reportOnlyNew; vmProps = getSystemProperties(); workerThread = new Thread(new Runnable() { @Override public void run() { try { while(!Thread.currentThread().isInterrupted()) { Runnable cmd0 = cmd.getAndSet(null); if (cmd0 != null) cmd0.run(); else Thread.sleep(WORKER_THREAD_SLEEP_TIME); } } catch (InterruptedException ignore) { // No-op. } } }, "upd-ver-checker"); workerThread.setDaemon(true); workerThread.start(); } catch (UnsupportedEncodingException e) { throw new IgniteCheckedException("Failed to encode.", e); } } /** * Gets system properties. * * @return System properties. */ private static String getSystemProperties() { try { StringWriter sw = new StringWriter(); try { IgniteSystemProperties.safeSnapshot().store(new PrintWriter(sw), ""); } catch (IOException ignore) { return null; } return sw.toString(); } catch (SecurityException ignore) { return null; } } /** * @param reportOnlyNew Whether or not to report only new version. */ void reportOnlyNew(boolean reportOnlyNew) { this.reportOnlyNew = reportOnlyNew; } /** * @param topSize Size of topology for license verification purpose. */ void topologySize(int topSize) { this.topSize = topSize; } /** * @return Latest version. */ String latestVersion() { return latestVer; } /** * Starts asynchronous process for retrieving latest version data. * * @param log Logger. */ void checkForNewVersion(IgniteLogger log) { assert log != null; log = log.getLogger(getClass()); try { cmd.set(new UpdateChecker(log)); } catch (RejectedExecutionException e) { U.error(log, "Failed to schedule a thread due to execution rejection (safely ignoring): " + e.getMessage()); } } /** * Logs out latest version notification if such was received and available. * * @param log Logger. */ void reportStatus(IgniteLogger log) { assert log != null; log = log.getLogger(getClass()); String latestVer = this.latestVer; String downloadUrl = this.downloadUrl; downloadUrl = downloadUrl != null ? downloadUrl : IgniteKernal.SITE; if (latestVer != null) if (latestVer.equals(ver)) { if (!reportOnlyNew) throttle(log, false, "Your version is up to date."); } else throttle(log, true, "New version is available at " + downloadUrl + ": " + latestVer); else if (!reportOnlyNew) throttle(log, false, "Update status is not available."); } /** * * @param log Logger to use. * @param warn Whether or not this is a warning. * @param msg Message to log. */ private void throttle(IgniteLogger log, boolean warn, String msg) { assert(log != null); assert(msg != null); long now = U.currentTimeMillis(); if (now - lastLog > THROTTLE_PERIOD) { if (!warn) U.log(log, msg); else { U.quiet(true, msg); if (log.isInfoEnabled()) log.warning(msg); } lastLog = now; } } /** * Stops update notifier. */ public void stop() { workerThread.interrupt(); } /** * Asynchronous checker of the latest version available. */ private class UpdateChecker extends GridWorker { /** Logger. */ private final IgniteLogger log; /** * Creates checked with given logger. * * @param log Logger. */ UpdateChecker(IgniteLogger log) { super(igniteInstanceName, "grid-version-checker", log); this.log = log.getLogger(getClass()); } /** {@inheritDoc} */ @Override protected void body() throws InterruptedException { try { String stackTrace = gw != null ? gw.userStackTrace() : null; String postParams = "igniteInstanceName=" + encode(igniteInstanceName, CHARSET) + (!F.isEmpty(UPD_STATUS_PARAMS) ? "&" + UPD_STATUS_PARAMS : "") + (topSize > 0 ? "&topSize=" + topSize : "") + (!F.isEmpty(stackTrace) ? "&stackTrace=" + encode(stackTrace, CHARSET) : "") + (!F.isEmpty(vmProps) ? "&vmProps=" + encode(vmProps, CHARSET) : "") + pluginsVers; URLConnection conn = new URL(UPDATE_NOTIFIER_URL).openConnection(); if (!isCancelled()) { conn.setDoOutput(true); conn.setRequestProperty("Accept-Charset", CHARSET); conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded;charset=" + CHARSET); conn.setConnectTimeout(3000); conn.setReadTimeout(3000); try { try (OutputStream os = conn.getOutputStream()) { os.write(postParams.getBytes(CHARSET)); } try (InputStream in = conn.getInputStream()) { if (in == null) return; BufferedReader reader = new BufferedReader(new InputStreamReader(in, CHARSET)); for (String line; (line = reader.readLine()) != null; ) { if (line.contains("version")) latestVer = obtainVersionFrom(line); else if (line.contains("downloadUrl")) downloadUrl = obtainDownloadUrlFrom(line); } } } catch (IOException e) { if (log.isDebugEnabled()) log.debug("Failed to connect to Ignite update server. " + e.getMessage()); } } } catch (Exception e) { if (log.isDebugEnabled()) log.debug("Unexpected exception in update checker. " + e.getMessage()); } } /** * Gets the version from the current {@code node}, if one exists. * * @param line Line which contains value for extract. * @param metaName Name for extract. * @return Version or {@code null} if one's not found. */ @Nullable private String obtainMeta(String metaName, String line) { assert line.contains(metaName); return line.substring(line.indexOf(metaName) + metaName.length()).trim(); } /** * Gets the version from the current {@code node}, if one exists. * * @param line Line which contains value for extract. * @return Version or {@code null} if one's not found. */ @Nullable private String obtainVersionFrom(String line) { return obtainMeta("version=", line); } /** * Gets the download url from the current {@code node}, if one exists. * * @param line Which contains value for extract. * @return download url or {@code null} if one's not found. */ @Nullable private String obtainDownloadUrlFrom(String line) { return obtainMeta("downloadUrl=", line); } } }