diff --git a/.gitignore b/.gitignore index e14c866b6e..6f6dd6136c 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ pom.xml.versionsBackup **/target **/Californium.properties **/.env +.instance_id diff --git a/application/build.gradle b/application/build.gradle index 75a0e52f69..25ad51d129 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import org.apache.tools.ant.filters.ReplaceTokens + buildscript { ext { osPackageVersion = "3.8.0" @@ -56,6 +59,7 @@ ospackage { // Copy the config files from("target/conf") { + exclude "${pkgName}.conf" fileType CONFIG | NOREPLACE fileMode 0754 into "conf" @@ -84,6 +88,14 @@ buildRpm { requires("java-1.8.0") + from("target/conf") { + include "${pkgName}.conf" + filter(ReplaceTokens, tokens: ['pkg.platform': 'rpm']) + fileType CONFIG | NOREPLACE + fileMode 0754 + into "${pkgInstallFolder}/conf" + } + preInstall file("${buildDir}/control/rpm/preinst") postInstall file("${buildDir}/control/rpm/postinst") preUninstall file("${buildDir}/control/rpm/prerm") @@ -113,6 +125,14 @@ buildDeb { requires("openjdk-8-jre").or("java8-runtime").or("oracle-java8-installer").or("openjdk-8-jre-headless") + from("target/conf") { + include "${pkgName}.conf" + filter(ReplaceTokens, tokens: ['pkg.platform': 'deb']) + fileType CONFIG | NOREPLACE + fileMode 0754 + into "${pkgInstallFolder}/conf" + } + configurationFile("${pkgInstallFolder}/conf/${pkgName}.conf") configurationFile("${pkgInstallFolder}/conf/${pkgName}.yml") configurationFile("${pkgInstallFolder}/conf/logback.xml") diff --git a/application/src/main/conf/thingsboard.conf b/application/src/main/conf/thingsboard.conf index 328054fe20..ef977eea46 100644 --- a/application/src/main/conf/thingsboard.conf +++ b/application/src/main/conf/thingsboard.conf @@ -14,6 +14,6 @@ # limitations under the License. # -export JAVA_OPTS="$JAVA_OPTS" +export JAVA_OPTS="$JAVA_OPTS -Dplatform=@pkg.platform@" export LOG_FILENAME=${pkg.name}.out export LOADER_PATH=${pkg.installFolder}/conf,${pkg.installFolder}/extensions diff --git a/application/src/main/java/org/thingsboard/server/controller/AdminController.java b/application/src/main/java/org/thingsboard/server/controller/AdminController.java index b17c394f76..bc3c47ebb6 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AdminController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AdminController.java @@ -22,6 +22,8 @@ import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.exception.ThingsboardException; import org.thingsboard.server.service.mail.MailService; +import org.thingsboard.server.service.update.UpdateService; +import org.thingsboard.server.service.update.model.UpdateMessage; @RestController @RequestMapping("/api/admin") @@ -33,6 +35,9 @@ public class AdminController extends BaseController { @Autowired private AdminSettingsService adminSettingsService; + @Autowired + private UpdateService updateService; + @PreAuthorize("hasAuthority('SYS_ADMIN')") @RequestMapping(value = "/settings/{key}", method = RequestMethod.GET) @ResponseBody @@ -72,4 +77,16 @@ public class AdminController extends BaseController { throw handleException(e); } } + + @PreAuthorize("hasAuthority('SYS_ADMIN')") + @RequestMapping(value = "/updates", method = RequestMethod.GET) + @ResponseBody + public UpdateMessage checkUpdates() throws ThingsboardException { + try { + return updateService.checkUpdates(); + } catch (Exception e) { + throw handleException(e); + } + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java new file mode 100644 index 0000000000..0856b8a168 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java @@ -0,0 +1,128 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed 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.thingsboard.server.service.update; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.thingsboard.server.service.update.model.UpdateMessage; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +@Service +@Slf4j +public class DefaultUpdateService implements UpdateService { + + private static final String INSTANCE_ID_FILE = ".instance_id"; + private static final String UPDATE_SERVER_BASE_URL = "https://updates.thingsboard.io"; + + private static final String PLATFORM_PARAM = "platform"; + private static final String VERSION_PARAM = "version"; + private static final String INSTANCE_ID_PARAM = "instanceId"; + + @Value("${updates.enabled}") + private boolean updatesEnabled; + + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + + private ScheduledFuture checkUpdatesFuture = null; + private RestTemplate restClient = new RestTemplate(); + + private UpdateMessage updateMessage; + + private String platform; + private String version; + private UUID instanceId = null; + + @PostConstruct + private void init() { + updateMessage = new UpdateMessage("", false); + if (updatesEnabled) { + try { + platform = System.getProperty("platform", "unknown"); + version = getClass().getPackage().getImplementationVersion(); + if (version == null) { + version = "unknown"; + } + Path instanceIdPath = Paths.get(INSTANCE_ID_FILE); + if (Files.exists(instanceIdPath)) { + byte[] data = Files.readAllBytes(instanceIdPath); + if (data != null && data.length > 0) { + try { + instanceId = UUID.fromString(new String(data)); + } catch (IllegalArgumentException e) { + } + } + } + if (instanceId == null) { + instanceId = UUID.randomUUID(); + Files.write(instanceIdPath, instanceId.toString().getBytes()); + } + checkUpdatesFuture = scheduler.scheduleAtFixedRate(checkUpdatesRunnable, 0, 1, TimeUnit.HOURS); + } catch (Exception e) {} + } + } + + @PreDestroy + private void destroy() { + try { + if (checkUpdatesFuture != null) { + checkUpdatesFuture.cancel(true); + } + scheduler.shutdownNow(); + } catch (Exception e) {} + } + + Runnable checkUpdatesRunnable = new Runnable() { + @Override + public void run() { + try { + log.trace("Executing check update method for instanceId [{}], platform [{}] and version [{}]", instanceId, platform, version); + ObjectNode request = new ObjectMapper().createObjectNode(); + request.put(PLATFORM_PARAM, platform); + request.put(VERSION_PARAM, version); + request.put(INSTANCE_ID_PARAM, instanceId.toString()); + JsonNode response = restClient.postForObject(UPDATE_SERVER_BASE_URL+"/api/thingsboard/updates", request, JsonNode.class); + updateMessage = new UpdateMessage( + response.get("message").asText(), + response.get("updateAvailable").asBoolean() + ); + } catch (Exception e) { + log.trace(e.getMessage()); + } + } + }; + + @Override + public UpdateMessage checkUpdates() { + return updateMessage; + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java b/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java new file mode 100644 index 0000000000..18bfb2f850 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/update/UpdateService.java @@ -0,0 +1,25 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed 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.thingsboard.server.service.update; + +import org.thingsboard.server.service.update.model.UpdateMessage; + +public interface UpdateService { + + UpdateMessage checkUpdates(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/update/model/UpdateMessage.java b/application/src/main/java/org/thingsboard/server/service/update/model/UpdateMessage.java new file mode 100644 index 0000000000..bbb85115e4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/update/model/UpdateMessage.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2017 The Thingsboard Authors + * + * Licensed 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.thingsboard.server.service.update.model; + +import lombok.Data; + +@Data +public class UpdateMessage { + + private final String message; + private final boolean isUpdateAvailable; + +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 42b37db793..778406aadf 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -184,3 +184,7 @@ cache: policy: "${CACHE_DEVICE_CREDENTIAL_MAX_SIZE_POLICY:PER_NODE}" size: "${CACHE_DEVICE_CREDENTIAL_MAX_SIZE_SIZE:1000000}" +# Check new version updates parameters +updates: + # Enable/disable updates checking. + enabled: "${UPDATES_ENABLED:true}" diff --git a/application/src/main/scripts/windows/service.xml b/application/src/main/scripts/windows/service.xml index b2acc45a8a..becbcdc9bd 100644 --- a/application/src/main/scripts/windows/service.xml +++ b/application/src/main/scripts/windows/service.xml @@ -7,6 +7,7 @@ rotate java + -Dplatform=windows -jar %BASE%\lib\${pkg.name}.jar diff --git a/ui/package.json b/ui/package.json index ed4b5b1e4d..a36bb09216 100644 --- a/ui/package.json +++ b/ui/package.json @@ -64,6 +64,7 @@ "ngreact": "^0.3.0", "objectpath": "^1.2.1", "oclazyload": "^1.0.9", + "raphael": "^2.2.7", "rc-select": "^6.6.1", "react": "^15.4.1", "react-ace": "^4.1.0", diff --git a/ui/src/app/api/admin.service.js b/ui/src/app/api/admin.service.js index cd41966cfb..15913979c9 100644 --- a/ui/src/app/api/admin.service.js +++ b/ui/src/app/api/admin.service.js @@ -23,7 +23,8 @@ function AdminService($http, $q) { var service = { getAdminSettings: getAdminSettings, saveAdminSettings: saveAdminSettings, - sendTestMail: sendTestMail + sendTestMail: sendTestMail, + checkUpdates: checkUpdates } return service; @@ -60,4 +61,15 @@ function AdminService($http, $q) { }); return deferred.promise; } + + function checkUpdates() { + var deferred = $q.defer(); + var url = '/api/admin/updates'; + $http.get(url, null).then(function success(response) { + deferred.resolve(response.data); + }, function fail() { + deferred.reject(); + }); + return deferred.promise; + } } diff --git a/ui/src/app/api/user.service.js b/ui/src/app/api/user.service.js index ea86cd6f36..516d8232bd 100644 --- a/ui/src/app/api/user.service.js +++ b/ui/src/app/api/user.service.js @@ -22,7 +22,7 @@ export default angular.module('thingsboard.api.user', [thingsboardApiLogin, .name; /*@ngInject*/ -function UserService($http, $q, $rootScope, store, jwtHelper, $translate, $state) { +function UserService($http, $q, $rootScope, adminService, toast, store, jwtHelper, $translate, $state) { var currentUser = null, currentUserDetails = null, userLoaded = false; @@ -382,6 +382,14 @@ function UserService($http, $q, $rootScope, store, jwtHelper, $translate, $state place = 'home.dashboards.dashboard'; params = {dashboardId: currentUserDetails.additionalInfo.defaultDashboardId}; } + } else if (currentUser.authority === 'SYS_ADMIN') { + adminService.checkUpdates().then( + function (updateMessage) { + if (updateMessage && updateMessage.updateAvailable) { + toast.showInfo(updateMessage.message, 0, null, 'bottom right'); + } + } + ); } $state.go(place, params); } else { diff --git a/ui/src/app/services/info-toast.tpl.html b/ui/src/app/services/info-toast.tpl.html new file mode 100644 index 0000000000..7a1f29d805 --- /dev/null +++ b/ui/src/app/services/info-toast.tpl.html @@ -0,0 +1,25 @@ + + +
+
+ + {{ 'action.close' | translate }} + +
+
diff --git a/ui/src/app/services/toast.scss b/ui/src/app/services/toast.scss index b1740f1007..5730d88a58 100644 --- a/ui/src/app/services/toast.scss +++ b/ui/src/app/services/toast.scss @@ -13,6 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +md-toast.tb-info-toast .md-toast-content { + font-size: 18px; + padding: 18px; + height: 100%; +} + md-toast.tb-success-toast .md-toast-content { font-size: 18px !important; background-color: green; diff --git a/ui/src/app/services/toast.service.js b/ui/src/app/services/toast.service.js index d9cd7b07e1..b18abda69c 100644 --- a/ui/src/app/services/toast.service.js +++ b/ui/src/app/services/toast.service.js @@ -15,6 +15,7 @@ */ /* eslint-disable import/no-unresolved, import/default */ +import infoToast from './info-toast.tpl.html'; import successToast from './success-toast.tpl.html'; import errorToast from './error-toast.tpl.html'; @@ -26,6 +27,7 @@ export default function Toast($mdToast, $document) { var showing = false; var service = { + showInfo: showInfo, showSuccess: showSuccess, showError: showError, hide: hide @@ -33,7 +35,15 @@ export default function Toast($mdToast, $document) { return service; + function showInfo(infoMessage, delay, toastParent, position) { + showMessage(infoToast, infoMessage, delay, toastParent, position); + } + function showSuccess(successMessage, delay, toastParent, position) { + showMessage(successToast, successMessage, delay, toastParent, position); + } + + function showMessage(templateUrl, message, delay, toastParent, position) { if (!toastParent) { toastParent = angular.element($document[0].getElementById('toast-parent')); } @@ -45,8 +55,8 @@ export default function Toast($mdToast, $document) { position: position, controller: 'ToastController', controllerAs: 'vm', - templateUrl: successToast, - locals: {message: successMessage}, + templateUrl: templateUrl, + locals: {message: message}, parent: toastParent }); }