From 4371a08efa321b3256c814092063d9f54c84743b Mon Sep 17 00:00:00 2001
From: kolena <kolena@avast.com>
Date: Sun, 30 Dec 2018 00:10:48 +0100
Subject: [PATCH] Minor refactoring and updater execution (still WIN only)

---
 .travis.sh                               |  10 +-
 README.md                                |  15 +-
 app/AppModule.scala                      |  37 +++--
 app/lib/App.scala                        |  10 +-
 app/{updater => lib}/AppVersion.scala    |   4 +-
 app/lib/client/clientapi.scala           |   3 +-
 app/lib/commands/CommandExecutor.scala   |   5 +-
 app/lib/server/CloudConnector.scala      |  10 +-
 app/updater/GithubConnector.scala        |   4 +-
 app/updater/ServiceUpdater.scala         |  39 -----
 app/updater/ServiceUpdaterExecutor.scala |  56 ++++++++
 app/updater/Updater.scala                |  90 +++++++-----
 app/utils/CirceImplicits.scala           |   3 +-
 build.sbt                                |  43 +++++-
 conf/reference.conf                      |   5 +-
 test/updater/GithubConnectorTest.scala   |   2 +-
 test/utils/AppVersionTest.scala          |   2 +-
 windeploy/rbackup-client-updater.bat     | 175 +++++++++++++++++++++++
 18 files changed, 382 insertions(+), 131 deletions(-)
 rename app/{updater => lib}/AppVersion.scala (96%)
 delete mode 100644 app/updater/ServiceUpdater.scala
 create mode 100644 app/updater/ServiceUpdaterExecutor.scala
 create mode 100644 windeploy/rbackup-client-updater.bat

diff --git a/.travis.sh b/.travis.sh
index be124ef..2af6874 100755
--- a/.travis.sh
+++ b/.travis.sh
@@ -31,12 +31,4 @@ function client_test() {
      docker-compose down
 }
 
-dir=$(pwd)
-
-client_test &&
-  if $(test "${TRAVIS_REPO_SLUG}" == "jendakol/rbackup-scala-client" && test "${TRAVIS_PULL_REQUEST}" == "false" && test "$TRAVIS_TAG" != ""); then
-    cd $pwd
-    sbt +publish
-  else
-    exit 0 # skipping publish, it's regular build
-  fi
+client_test
diff --git a/README.md b/README.md
index f656e38..72542d3 100644
--- a/README.md
+++ b/README.md
@@ -2,4 +2,17 @@
 
 This is Scala implementation of client for [RBackup](https://github.com/jendakol/rbackup).
 
-Readme TBD :-)
\ No newline at end of file
+Readme TBD :-)
+
+## Build (release)
+```
+#!/usr/bin/fish
+
+./.travis.sh
+
+env VERSION=$argv[1] \
+    SENTRY_DSN="https://abcd@sentry.io/1234" \
+    sbt ";clean;setVersionInSources;setSentryDsnInSources;dist"
+```
+
+The SENTRY_DSN is optional.
\ No newline at end of file
diff --git a/app/AppModule.scala b/app/AppModule.scala
index 7d3c05e..e5954fb 100644
--- a/app/AppModule.scala
+++ b/app/AppModule.scala
@@ -20,7 +20,7 @@ import org.http4s.client.middleware.FollowRedirect
 import play.api.{Configuration, Environment}
 import scalikejdbc._
 import scalikejdbc.config.DBs
-import updater._
+import updater.{GithubConnector, LinuxServiceUpdaterExecutor, ServiceUpdaterExecutor, WindowsServiceUpdaterExecutor}
 import utils.AllowedWsApiOrigins
 
 import scala.collection.JavaConverters._
@@ -33,15 +33,21 @@ class AppModule(environment: Environment, configuration: Configuration)
     with StrictLogging {
   private val config = configuration.underlying
 
-  if (config.getBoolean("sentry.enabled") && config.getString("sentry.environment") != "dev") {
-    logger.info("Sentry configured")
-    val sentry = Sentry.init(config.getString("sentry.dsn"))
-    sentry.setRelease(App.versionStr)
-    sentry.setEnvironment(config.getString("sentry.environment"))
-    sentry.setServerName(config.getString("deviceId"))
-    sentry.setDist(config.getString("sentry.dist"))
-  } else {
-    logger.info("Sentry NOT configured")
+  App.SentryDsn match {
+    case Some(dsn) =>
+      if (config.getBoolean("sentry.enabled") && config.getString("environment") != "dev") {
+        logger.info("Sentry configured")
+        val sentry = Sentry.init(dsn)
+        sentry.setRelease(App.versionStr)
+        sentry.addTag("app", "client")
+        sentry.setEnvironment(config.getString("environment"))
+        sentry.setServerName(config.getString("deviceId"))
+        sentry.setDist(if (SystemUtils.IS_OS_WINDOWS) "win" else "linux")
+      } else {
+        logger.info("Sentry NOT enabled")
+      }
+
+    case None => logger.info("Sentry NOT configured")
   }
 
   DBs.setupAll()
@@ -67,10 +73,13 @@ class AppModule(environment: Environment, configuration: Configuration)
 
     bind[AllowedWsApiOrigins].toInstance(AllowedWsApiOrigins(config.getStringList("allowedWsApiOrigins").asScala))
 
+    val deviceId = DeviceId(config.getString("deviceId"))
+    bind[DeviceId].toInstance(deviceId)
+
     val cloudConnector = CloudConnector.fromConfig(config.getConfig("cloudConnector"), blockingScheduler)
     val dao = new Dao(blockingScheduler)
     val settings = new Settings(dao)
-    val stateManager = new StateManager(DeviceId(config.getString("deviceId")), cloudConnector, dao, settings)
+    val stateManager = new StateManager(deviceId, cloudConnector, dao, settings)
 
     bind[CloudConnector].toInstance(cloudConnector)
     bind[Dao].toInstance(dao)
@@ -116,9 +125,9 @@ class AppModule(environment: Environment, configuration: Configuration)
 
   private def bindServiceUpdater(): Unit = {
     val updater = if (SystemUtils.IS_OS_WINDOWS) {
-      new WindowsServiceUpdater
-    } else new LinuxServiceUpdater
+      new WindowsServiceUpdaterExecutor
+    } else new LinuxServiceUpdaterExecutor
 
-    bind[ServiceUpdater].toInstance(updater)
+    bind[ServiceUpdaterExecutor].toInstance(updater)
   }
 }
diff --git a/app/lib/App.scala b/app/lib/App.scala
index 5773218..56c3166 100644
--- a/app/lib/App.scala
+++ b/app/lib/App.scala
@@ -13,7 +13,7 @@ import monix.eval.Task
 import monix.execution.{Cancelable, Scheduler}
 import org.http4s.Uri
 import play.api.inject.ApplicationLifecycle
-import updater.{AppVersion, Updater}
+import updater.Updater
 
 import scala.collection.generic.CanBuildFrom
 import scala.concurrent.Future
@@ -38,9 +38,11 @@ class App @Inject()(backupSetsExecutor: BackupSetsExecutor, updater: Updater)(li
 }
 
 object App {
-  final val versionStr: String = "0.1.2"
+  final val versionStr: String = "0.1.3"
   final val version: AppVersion = AppVersion(versionStr).getOrElse(throw new IllegalArgumentException("Could not parse versionStr"))
 
+  final val SentryDsn: Option[String] = Some("https://74fe77b3e5024c18bd850d09c4c775c4@sentry.io/1340234")
+
   type Result[A] = EitherT[Task, AppException, A]
 
   def pureResult[A](a: => A): Result[A] = {
@@ -205,4 +207,6 @@ object App {
 
 case class ServerSession(rootUri: Uri, sessionId: String, serverVersion: AppVersion)
 
-case class DeviceId(value: String) extends AnyVal
+case class DeviceId(value: String) {
+  override def toString: String = value
+}
diff --git a/app/updater/AppVersion.scala b/app/lib/AppVersion.scala
similarity index 96%
rename from app/updater/AppVersion.scala
rename to app/lib/AppVersion.scala
index 5cd1651..8b69e0a 100644
--- a/app/updater/AppVersion.scala
+++ b/app/lib/AppVersion.scala
@@ -1,7 +1,7 @@
-package updater
+package lib
 
 import lib.AppException.ParsingFailure
-import updater.AppVersion._
+import lib.AppVersion._
 
 case class AppVersion(major: Int, minor: Int, build: Int, suffix: Option[String] = None) {
   def >(other: AppVersion): Boolean = {
diff --git a/app/lib/client/clientapi.scala b/app/lib/client/clientapi.scala
index fb75953..221bc35 100644
--- a/app/lib/client/clientapi.scala
+++ b/app/lib/client/clientapi.scala
@@ -10,14 +10,13 @@ import com.typesafe.scalalogging.StrictLogging
 import io.circe.generic.extras.Configuration
 import io.circe.syntax._
 import io.circe.{Decoder, Json}
-import lib.App
+import lib.{App, AppVersion}
 import lib.App._
 import lib.App.StringOps
 import lib.client.clientapi.FileTreeNode.{Directory, RegularFile}
 import lib.server.serverapi
 import lib.server.serverapi.{RemoteFile, RemoteFileVersion}
 import org.http4s.Uri
-import updater.AppVersion
 
 object clientapi extends StrictLogging {
 
diff --git a/app/lib/commands/CommandExecutor.scala b/app/lib/commands/CommandExecutor.scala
index 4ecf43b..6b86e38 100644
--- a/app/lib/commands/CommandExecutor.scala
+++ b/app/lib/commands/CommandExecutor.scala
@@ -24,7 +24,6 @@ import monix.eval.Task
 import monix.execution.Scheduler
 import org.http4s.Uri
 import utils.CirceImplicits._
-import utils.ConfigProperty
 
 @Singleton
 class CommandExecutor @Inject()(cloudConnector: CloudConnector,
@@ -35,7 +34,7 @@ class CommandExecutor @Inject()(cloudConnector: CloudConnector,
                                 fileCommandExecutor: FileCommandExecutor,
                                 settings: Settings,
                                 stateManager: StateManager,
-                                @ConfigProperty("deviceId") deviceId: String)(implicit scheduler: Scheduler)
+                                deviceId: DeviceId)(implicit scheduler: Scheduler)
     extends StrictLogging {
 
   wsApiController.setEventCallback(processEvent)
@@ -51,7 +50,7 @@ class CommandExecutor @Inject()(cloudConnector: CloudConnector,
 
         import scala.concurrent.duration._
 
-        tasksManager.start(RunningTask.FileUpload("theName"))(EitherT.right(Task.unit.delayResult(10.seconds))) >>
+        tasksManager.start(RunningTask.FileUpload(deviceId.value))(EitherT.right(Task.unit.delayResult(10.seconds))) >>
           cloudConnector.status
             .flatMap { str =>
               parse(s"""{"serverResponse": "$str"}""").toResult
diff --git a/app/lib/server/CloudConnector.scala b/app/lib/server/CloudConnector.scala
index 42190c4..05540a8 100644
--- a/app/lib/server/CloudConnector.scala
+++ b/app/lib/server/CloudConnector.scala
@@ -15,7 +15,7 @@ import io.circe.Json
 import io.circe.generic.extras.auto._
 import lib.App._
 import lib.AppException.ServerNotResponding
-import lib._
+import lib.{AppVersion, _}
 import lib.server.serverapi.ListFilesResponse.FilesList
 import lib.server.serverapi._
 import monix.eval.Task
@@ -28,7 +28,6 @@ import org.http4s.multipart._
 import org.http4s.{Headers, Method, Request, Response, Status, Uri}
 import pureconfig.modules.http4s.uriReader
 import pureconfig.{CamelCase, ConfigFieldMapping, ProductHint}
-import updater.AppVersion
 import utils.CirceImplicits._
 import utils.{FileCopier, InputStreamWithSha256, StatsInputStream, StatsOutputStream}
 
@@ -50,12 +49,15 @@ class CloudConnector(httpClient: Client[Task], chunkSize: Int, blockingScheduler
     }
   }
 
-  def login(rootUri: Uri, deviceId: String, username: String, password: String): Result[LoginResponse] = {
+  def login(rootUri: Uri, deviceId: DeviceId, username: String, password: String): Result[LoginResponse] = {
     logger.debug(s"Logging device $deviceId with username $username")
     App.leaveBreadcrumb("Logging in", Map("uri" -> rootUri, "username" -> username))
 
     val request =
-      plainRequestToHost(Method.GET, rootUri, "account/login", Map("device_id" -> deviceId, "username" -> username, "password" -> password))
+      plainRequestToHost(Method.GET,
+                         rootUri,
+                         "account/login",
+                         Map("device_id" -> deviceId.value, "username" -> username, "password" -> password))
 
     status(rootUri).flatMap { status =>
       exec(request) {
diff --git a/app/updater/GithubConnector.scala b/app/updater/GithubConnector.scala
index 2c33336..f2d3f3f 100644
--- a/app/updater/GithubConnector.scala
+++ b/app/updater/GithubConnector.scala
@@ -10,14 +10,14 @@ import cats.syntax.all._
 import com.typesafe.scalalogging.StrictLogging
 import io.circe.Decoder
 import lib.App._
-import lib.AppException
 import lib.AppException._
+import lib.{AppException, AppVersion}
 import monix.eval.Task
 import monix.execution.Scheduler
 import org.http4s.client.Client
 import org.http4s.headers.`Content-Length`
 import org.http4s.{Response, Status, Uri}
-import updater.GithubConnector._
+import GithubConnector._
 
 import scala.concurrent.ExecutionContext
 
diff --git a/app/updater/ServiceUpdater.scala b/app/updater/ServiceUpdater.scala
deleted file mode 100644
index eb393bd..0000000
--- a/app/updater/ServiceUpdater.scala
+++ /dev/null
@@ -1,39 +0,0 @@
-package updater
-
-import better.files.File
-import com.typesafe.scalalogging.StrictLogging
-
-import scala.language.postfixOps
-
-sealed trait ServiceUpdater {
-  def restartAndReplace(dirWithUpdate: File): Unit
-}
-
-class WindowsServiceUpdater extends ServiceUpdater with StrictLogging {
-  override def restartAndReplace(dirWithUpdate: File): Unit = {
-    Runtime.getRuntime.exec(
-      Array(
-        "cmd",
-        "/C",
-        "start",
-        "\"\"",
-        "restart_replace.cmd",
-        dirWithUpdate.pathAsString
-      ))
-    ()
-  }
-}
-
-class LinuxServiceUpdater extends ServiceUpdater {
-  override def restartAndReplace(dirWithUpdate: File): Unit = {
-    Runtime.getRuntime.exec(
-      Array(
-        "/bin/bash",
-        "-c",
-        "restart_replace.sh",
-        dirWithUpdate.pathAsString,
-        "&",
-      ))
-    ()
-  }
-}
diff --git a/app/updater/ServiceUpdaterExecutor.scala b/app/updater/ServiceUpdaterExecutor.scala
new file mode 100644
index 0000000..5fd61e6
--- /dev/null
+++ b/app/updater/ServiceUpdaterExecutor.scala
@@ -0,0 +1,56 @@
+package updater
+
+import better.files.File
+import com.typesafe.scalalogging.StrictLogging
+import lib.{AppVersion, DeviceId}
+
+import scala.language.postfixOps
+
+sealed trait ServiceUpdaterExecutor {
+  def executeUpdate(currentVersion: AppVersion, newVersion: AppVersion, env: String, deviceId: DeviceId, dirWithUpdate: File): Unit
+}
+
+class WindowsServiceUpdaterExecutor extends ServiceUpdaterExecutor with StrictLogging {
+  override def executeUpdate(currentVersion: AppVersion,
+                             newVersion: AppVersion,
+                             env: String,
+                             deviceId: DeviceId,
+                             dirWithUpdate: File): Unit = {
+    logger.info(s"Starting the update with args: $currentVersion, $newVersion, $env, $deviceId, $dirWithUpdate")
+
+    Runtime.getRuntime.exec(
+      Array(
+        "cmd",
+        "/C",
+        "start",
+        "\"\"",
+        "java",
+        "-jar",
+        "updater.jar",
+        currentVersion.toString,
+        newVersion.toString,
+        env,
+        deviceId.value,
+        dirWithUpdate.pathAsString
+      ))
+    ()
+  }
+}
+
+class LinuxServiceUpdaterExecutor extends ServiceUpdaterExecutor {
+  override def executeUpdate(currentVersion: AppVersion,
+                             newVersion: AppVersion,
+                             env: String,
+                             deviceId: DeviceId,
+                             dirWithUpdate: File): Unit = {
+    Runtime.getRuntime.exec(
+      Array(
+        "/bin/bash",
+        "-c",
+        "restart_replace.sh",
+        dirWithUpdate.pathAsString,
+        "&",
+      ))
+    ()
+  }
+}
diff --git a/app/updater/Updater.scala b/app/updater/Updater.scala
index 73e4a2f..769e027 100644
--- a/app/updater/Updater.scala
+++ b/app/updater/Updater.scala
@@ -1,5 +1,6 @@
 package updater
 
+import java.net.ConnectException
 import java.util.concurrent.atomic.AtomicBoolean
 
 import better.files.File
@@ -8,16 +9,20 @@ import com.typesafe.scalalogging.StrictLogging
 import javax.inject.{Inject, Named, Singleton}
 import lib.App._
 import lib.AppException.UpdateException
+import lib.{App, AppVersion, DeviceId}
 import monix.eval.Task
 import monix.execution.{Cancelable, Scheduler}
 import updater.GithubConnector.Release
+import utils.ConfigProperty
 
 import scala.concurrent.duration._
 import scala.util.control.NonFatal
 
 @Singleton
 class Updater @Inject()(connector: GithubConnector,
-                        serviceUpdater: ServiceUpdater,
+                        serviceUpdater: ServiceUpdaterExecutor,
+                        @ConfigProperty("environment") env: String,
+                        deviceId: DeviceId,
                         @Named("updaterCheckPeriod") checkPeriod: FiniteDuration)(implicit scheduler: Scheduler)
     extends StrictLogging {
   private val updateRunning = new AtomicBoolean(false)
@@ -26,45 +31,49 @@ class Updater @Inject()(connector: GithubConnector,
     logger.info(s"Started updates checker, will check every $checkPeriod")
 
     scheduler.scheduleAtFixedRate(1.seconds, checkPeriod) {
-      connector.checkUpdate
-        .flatMap {
-          case Some(rel) =>
-            if (updateRunning.compareAndSet(false, true)) {
-              logger.debug(s"Found update: $rel")
-              updateApp(rel)
-            } else pureResult(())
-
-          case None =>
-            logger.debug("Didn't find update for current version")
-            pureResult(())
-        }
-        .recoverWith {
-          case ae =>
-            updateRunning.set(false)
-            logger.warn("Could not download update", ae)
-            EitherT.leftT[Task, Unit](ae)
-        }
-        .value
-        .onErrorRecover {
-          case e: java.net.ConnectException =>
-            updateRunning.set(false)
-            logger.warn("Could not download update", e)
-            Left(UpdateException("Could not update the app", e))
-
-          case e: UpdateException =>
-            updateRunning.set(false)
-            logger.warn("Could not download update", e)
-            Left(e)
-
-          case NonFatal(e) =>
-            updateRunning.set(false)
-            logger.warn("Unknown error while updating the app", e)
-            Left(UpdateException("Could not update the app", e))
-        }
+      tryUpdate.value
         .runSyncUnsafe(Duration.Inf)
     }
   }
 
+  def tryUpdate: Result[Unit] = EitherT {
+    connector.checkUpdate
+      .flatMap {
+        case Some(rel) =>
+          if (updateRunning.compareAndSet(false, true)) {
+            logger.debug(s"Found update: $rel")
+            updateApp(rel)
+          } else pureResult(())
+
+        case None =>
+          logger.debug("Didn't find update for current version")
+          pureResult(())
+      }
+      .recoverWith {
+        case ae =>
+          updateRunning.set(false)
+          logger.warn("Could not download update", ae)
+          EitherT.leftT[Task, Unit](ae)
+      }
+      .value
+      .onErrorRecover {
+        case e: ConnectException =>
+          updateRunning.set(false)
+          logger.warn("Could not download update", e)
+          Left(UpdateException("Could not update the app", e))
+
+        case e: UpdateException =>
+          updateRunning.set(false)
+          logger.warn("Could not download update", e)
+          Left(e)
+
+        case NonFatal(e) =>
+          updateRunning.set(false)
+          logger.warn("Unknown error while updating the app", e)
+          Left(UpdateException("Could not update the app", e))
+      }
+  }
+
   private def updateApp(release: Release): Result[Unit] = {
     // TODO setting
 
@@ -83,8 +92,13 @@ class Updater @Inject()(connector: GithubConnector,
 
       // TODO don't do it if task is running
 
-      logger.info("Starting the update")
-      serviceUpdater.restartAndReplace(dirWithUpdate)
+      val newVersion = release.appVersion.getOrElse {
+        AppVersion(0, 0, 0)
+      }
+
+      logger.info(s"Starting the update from ${App.version} to $newVersion")
+
+      serviceUpdater.executeUpdate(App.version, newVersion, env, deviceId, dirWithUpdate)
     }
   }
 
diff --git a/app/utils/CirceImplicits.scala b/app/utils/CirceImplicits.scala
index 0f818f7..b19ff43 100644
--- a/app/utils/CirceImplicits.scala
+++ b/app/utils/CirceImplicits.scala
@@ -6,9 +6,8 @@ import java.util.UUID
 import cats.syntax.either._
 import io.circe.generic.extras.Configuration
 import io.circe.{Decoder, Encoder}
-import lib.DeviceId
+import lib.{AppVersion, DeviceId}
 import org.http4s.Uri
-import updater.AppVersion
 
 object CirceImplicits {
   implicit val configuration: Configuration = Configuration.default.withSnakeCaseConstructorNames.withSnakeCaseMemberNames
diff --git a/build.sbt b/build.sbt
index 09f30d9..e0f0900 100644
--- a/build.sbt
+++ b/build.sbt
@@ -13,7 +13,7 @@ mappings in Universal ++= directory(baseDirectory.value / "public")
 
 name := "rbackup-client"
 
-version := sys.env.getOrElse("TRAVIS_TAG", "0.1.0")
+version := sys.env.getOrElse("VERSION", "0.1.0")
 
 scalaVersion := "2.12.7"
 
@@ -37,9 +37,13 @@ libraryDependencies ++= Seq(
   "io.circe" %% "circe-generic-extras" % "0.10.1",
   "com.avast.metrics" %% "metrics-scala" % Versions.metricsVersion,
   "com.avast.metrics" % "metrics-statsd" % Versions.metricsVersion,
+  "com.softwaremill.sttp" %% "core" % "1.5.1",
+  "org.apache.commons" % "commons-lang3" % "3.8.1",
+  "com.github.pathikrit" %% "better-files" % "3.6.0",
+  "org.typelevel" %% "cats-core" % "1.5.0",
   "io.sentry" % "sentry-logback" % "1.7.14",
   "com.typesafe.scala-logging" %% "scala-logging" % "3.9.0",
-  "org.scalatest" %% "scalatest" % "3.0.5" % "test",
+  "org.scalatest" %% "scalatest" % "3.0.5",
   "org.mockito" % "mockito-core" % "2.23.0" % "test"
 )
 
@@ -83,26 +87,51 @@ frontEndBuild := (frontEndBuild dependsOn cleanFrontEndBuild).value
 
 dist := (dist dependsOn frontEndBuild).value
 
-lazy val setVersionInSources = taskKey[Unit]("Sets build version into")
+lazy val AppModulePath = "app/lib/App.scala"
+
+
+lazy val setVersionInSources = taskKey[Unit]("Sets build version into sources")
 
 setVersionInSources := {
   import java.io.PrintWriter
   import scala.io.Source
   
-  val version = sys.env.getOrElse("TRAVIS_TAG", "0.1.0")
+  val version = sys.env.getOrElse("VERSION", throw new IllegalArgumentException("Missing VERSION env property"))
   println(s"Setting app version to $version")
   
-  val src = Source.fromFile("app/lib/App.scala").mkString
+  val src = Source.fromFile(AppModulePath).mkString
   val updated = src.replaceAll(
     """final val versionStr: String = "\d+.\d+.\d+"""",
     s"""final val versionStr: String = "$version""""
   )
-  
-  val writer = new PrintWriter(new File("app/lib/App.scala"))
+
+  val writer = new PrintWriter(new File(AppModulePath))
   writer.write(updated)
   writer.close()
 }
 
+lazy val setSentryDsnInSources = taskKey[Unit]("Sets Sentry DSN into sources")
+
+setSentryDsnInSources := {
+  import java.io.PrintWriter
+
+  import scala.io.Source
+
+  sys.env.get("SENTRY_DSN").foreach { dsn =>
+    println(s"Setting Sentry DSN")
+
+    val src = Source.fromFile(AppModulePath).mkString
+    val updated = src.replace(
+      """SentryDsn: Option[String] = None""",
+      s"""SentryDsn: Option[String] = Some("$dsn")"""
+    )
+
+    val writer = new PrintWriter(new File(AppModulePath))
+    writer.write(updated)
+    writer.close()
+  }
+}
+
 sources in (Compile, doc) := Seq.empty
 publishArtifact in (Compile, packageDoc) := false
 
diff --git a/conf/reference.conf b/conf/reference.conf
index d7176ba..8797814 100644
--- a/conf/reference.conf
+++ b/conf/reference.conf
@@ -1,4 +1,5 @@
 // deviceId = "" // REQUIRED
+environment = "prod"
 
 cloudConnectorDefaults {
   requestTimeout = 30 minutes
@@ -21,9 +22,7 @@ updater {
 }
 
 sentry {
-  enabled = false
-  // dsn = ""
-  environment = "prod"
+  enabled = true
 }
 
 allowedWsApiOrigins = ["http://localhost:3370"]
diff --git a/test/updater/GithubConnectorTest.scala b/test/updater/GithubConnectorTest.scala
index 13bfea7..06e3cab 100644
--- a/test/updater/GithubConnectorTest.scala
+++ b/test/updater/GithubConnectorTest.scala
@@ -2,7 +2,7 @@ package updater
 
 import java.time.Instant
 
-import lib.AppException
+import lib.{AppException, AppVersion}
 import monix.eval.Task
 import monix.execution.Scheduler.Implicits.global
 import org.http4s.client.Client
diff --git a/test/utils/AppVersionTest.scala b/test/utils/AppVersionTest.scala
index ba66dd9..9913732 100644
--- a/test/utils/AppVersionTest.scala
+++ b/test/utils/AppVersionTest.scala
@@ -1,8 +1,8 @@
 package utils
 
 import lib.AppException.ParsingFailure
+import lib.AppVersion
 import org.scalatest.FunSuite
-import updater.AppVersion
 
 class AppVersionTest extends FunSuite {
   test("parse from tag name") {
diff --git a/windeploy/rbackup-client-updater.bat b/windeploy/rbackup-client-updater.bat
new file mode 100644
index 0000000..5677397
--- /dev/null
+++ b/windeploy/rbackup-client-updater.bat
@@ -0,0 +1,175 @@
+@REM rbackup-client launcher script
+@REM
+@REM Environment:
+@REM JAVA_HOME - location of a JDK home dir (optional if java on path)
+@REM CFG_OPTS  - JVM options (optional)
+@REM Configuration:
+@setlocal enabledelayedexpansion
+
+@echo off
+
+
+if "%RBACKUP_CLIENT_UPDATER_HOME%"=="" (
+  set "APP_HOME=%~dp0\\.."
+
+  rem Also set the old env name for backwards compatibility
+  set "RBACKUP_CLIENT_UPDATER_HOME=%~dp0\\.."
+) else (
+  set "APP_HOME=%RBACKUP_CLIENT_UPDATER_HOME%"
+)
+
+set "APP_LIB_DIR=%APP_HOME%\lib\"
+
+rem Detect if we were double clicked, although theoretically A user could
+rem manually run cmd /c
+for %%x in (!cmdcmdline!) do if %%~x==/c set DOUBLECLICKED=1
+
+rem FIRST we load the config file of extra options.
+set "CFG_FILE=%APP_HOME%\RBACKUP_CLIENT_UPDATER_CONFIG"
+set CFG_OPTS=
+call :parse_config "%CFG_FILE%" CFG_OPTS
+
+rem We use the value of the JAVACMD environment variable if defined
+set _JAVACMD=%JAVACMD%
+
+if "%_JAVACMD%"=="" (
+  if not "%JAVA_HOME%"=="" (
+    if exist "%JAVA_HOME%\bin\java.exe" set "_JAVACMD=%JAVA_HOME%\bin\java.exe"
+  )
+)
+
+if "%_JAVACMD%"=="" set _JAVACMD=java
+
+rem Detect if this java is ok to use.
+for /F %%j in ('"%_JAVACMD%" -version  2^>^&1') do (
+  if %%~j==java set JAVAINSTALLED=1
+  if %%~j==openjdk set JAVAINSTALLED=1
+)
+
+rem BAT has no logical or, so we do it OLD SCHOOL! Oppan Redmond Style
+set JAVAOK=true
+if not defined JAVAINSTALLED set JAVAOK=false
+
+if "%JAVAOK%"=="false" (
+  echo.
+  echo A Java JDK is not installed or can't be found.
+  if not "%JAVA_HOME%"=="" (
+    echo JAVA_HOME = "%JAVA_HOME%"
+  )
+  echo.
+  echo Please go to
+  echo   http://www.oracle.com/technetwork/java/javase/downloads/index.html
+  echo and download a valid Java JDK and install before running rbackup-client.
+  echo.
+  echo If you think this message is in error, please check
+  echo your environment variables to see if "java.exe" and "javac.exe" are
+  echo available via JAVA_HOME or PATH.
+  echo.
+  if defined DOUBLECLICKED pause
+  exit /B 1
+)
+
+
+rem We use the value of the JAVA_OPTS environment variable if defined, rather than the config.
+set _JAVA_OPTS=%JAVA_OPTS%
+if "!_JAVA_OPTS!"=="" set _JAVA_OPTS=!CFG_OPTS!
+
+rem We keep in _JAVA_PARAMS all -J-prefixed and -D-prefixed arguments
+rem "-J" is stripped, "-D" is left as is, and everything is appended to JAVA_OPTS
+set _JAVA_PARAMS=
+set _APP_ARGS=
+
+set "APP_CLASSPATH=%APP_HOME%\conf\;%APP_LIB_DIR%\*"
+set "APP_MAIN_CLASS=lib.updater.app.Main"
+set "SCRIPT_CONF_FILE=%APP_HOME%\conf\application.ini"
+
+rem if configuration files exist, prepend their contents to the script arguments so it can be processed by this runner
+call :parse_config "%SCRIPT_CONF_FILE%" SCRIPT_CONF_ARGS
+
+call :process_args %SCRIPT_CONF_ARGS% %%*
+
+set _JAVA_OPTS=!_JAVA_OPTS! !_JAVA_PARAMS!
+
+set MAIN_CLASS=!APP_MAIN_CLASS!
+
+rem Call the application and pass all arguments unchanged.
+"%_JAVACMD%" !_JAVA_OPTS! !RBACKUP_CLIENT_UPDATER_OPTS! -cp "%APP_CLASSPATH%" %MAIN_CLASS% !_APP_ARGS!
+
+@endlocal
+
+exit /B %ERRORLEVEL%
+
+
+rem Loads a configuration file full of default command line options for this script.
+rem First argument is the path to the config file.
+rem Second argument is the name of the environment variable to write to.
+:parse_config
+  set _PARSE_FILE=%~1
+  set _PARSE_OUT=
+  if exist "%_PARSE_FILE%" (
+    FOR /F "tokens=* eol=# usebackq delims=" %%i IN ("%_PARSE_FILE%") DO (
+      set _PARSE_OUT=!_PARSE_OUT! %%i
+    )
+  )
+  set %2=!_PARSE_OUT!
+exit /B 0
+
+
+:add_java
+  set _JAVA_PARAMS=!_JAVA_PARAMS! %*
+exit /B 0
+
+
+:add_app
+  set _APP_ARGS=!_APP_ARGS! %*
+exit /B 0
+
+
+rem Processes incoming arguments and places them in appropriate global variables
+:process_args
+  :param_loop
+  call set _PARAM1=%%1
+  set "_TEST_PARAM=%~1"
+
+  if ["!_PARAM1!"]==[""] goto param_afterloop
+
+
+  rem ignore arguments that do not start with '-'
+  if "%_TEST_PARAM:~0,1%"=="-" goto param_java_check
+  set _APP_ARGS=!_APP_ARGS! !_PARAM1!
+  shift
+  goto param_loop
+
+  :param_java_check
+  if "!_TEST_PARAM:~0,2!"=="-J" (
+    rem strip -J prefix
+    set _JAVA_PARAMS=!_JAVA_PARAMS! !_TEST_PARAM:~2!
+    shift
+    goto param_loop
+  )
+
+  if "!_TEST_PARAM:~0,2!"=="-D" (
+    rem test if this was double-quoted property "-Dprop=42"
+    for /F "delims== tokens=1,*" %%G in ("!_TEST_PARAM!") DO (
+      if not ["%%H"] == [""] (
+        set _JAVA_PARAMS=!_JAVA_PARAMS! !_PARAM1!
+      ) else if [%2] neq [] (
+        rem it was a normal property: -Dprop=42 or -Drop="42"
+        call set _PARAM1=%%1=%%2
+        set _JAVA_PARAMS=!_JAVA_PARAMS! !_PARAM1!
+        shift
+      )
+    )
+  ) else (
+    if "!_TEST_PARAM!"=="-main" (
+      call set CUSTOM_MAIN_CLASS=%%2
+      shift
+    ) else (
+      set _APP_ARGS=!_APP_ARGS! !_PARAM1!
+    )
+  )
+  shift
+  goto param_loop
+  :param_afterloop
+
+exit /B 0