Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wip/windows service #329

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,11 @@ trait GenericPackageSettings
for {
(file, name) <- mappings
if !file.isDirectory
} yield ComponentFile(name, editable = (name startsWith "conf"))
} yield {
//For `editable` replaced startWith by endsWith. Like this it makes more sense to me (for `.conf` files) but not sure
//Add boolean for windows services (see ComponentFile)
ComponentFile(name, editable = (name endsWith "conf"), isWindowsService = (name endsWith "service.exe"))
}
val corePackage =
WindowsFeature(
id = WixHelper.cleanStringForId(name + "_core").takeRight(38), // Must be no longer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import SbtNativePackager._
import com.typesafe.sbt.packager.linux.{ LinuxFileMetaData, LinuxPackageMapping, LinuxSymlink, LinuxPlugin }
import com.typesafe.sbt.packager.debian.DebianPlugin
import com.typesafe.sbt.packager.rpm.RpmPlugin
//TODO wrap `mappings` to avoid name conflict? Something like `WinswFileMapping` ? I don't know yet how to do this will see later
import com.typesafe.sbt.packager.windows.Keys.{ mappings }

/**
* This class contains the default settings for creating and deploying an archetypical Java application.
Expand All @@ -23,7 +25,7 @@ object JavaServerAppPackaging {
import ServerLoader._
import LinuxPlugin.Users

def settings: Seq[Setting[_]] = JavaAppPackaging.settings ++ linuxSettings ++ debianSettings ++ rpmSettings
def settings: Seq[Setting[_]] = JavaAppPackaging.settings ++ linuxSettings ++ debianSettings ++ rpmSettings ++ windowsSettings
protected def etcDefaultTemplateSource: java.net.URL = getClass.getResource("etc-default-template")

private[this] def makeStartScriptReplacements(
Expand Down Expand Up @@ -80,6 +82,41 @@ object JavaServerAppPackaging {
}
}

/**
* Experimental Windows settings
*
*/
def windowsSettings: Seq[Setting[_]] = {

//QUESTION : not sure about `Windows` scope.
// My original idea was to provide winsw config with MSI but also with ZIP file
// Maybe something like :
// windows:packageBin -> generate MSI with windows service files but create a separate Windows feature to not install the Windows Service
// windows:packageZip -> generate ZIP with windows service files

Seq(
//get WinswExe file
getWinswExe <<= (target in Windows) map doGetWinswExe,
//TODO use batScriptExtraDefines ? Definitly it's not generic enough and this must be review
//Create the xml file that go with winsw exe
createWinswXml <<= (packageName, Keys.mainClass in Compile, scriptClasspath, target in Windows) map doCreateWinswXml,
/*
* Mappings exe and xml files
*/
//Exe
mappings in Windows <++= (getWinswExe, packageName) map { (file, name) =>
for {
s <- file.toSeq
} yield s -> ("bin/" + name + "_service.exe")
},
//Xml
mappings in Windows <++= (createWinswXml, packageName) map { (file, name) =>
for {
s <- file.toSeq
} yield s -> ("bin/" + name + "_service.xml")
})
}

/**
* general settings which apply to all linux server archetypes
*
Expand Down Expand Up @@ -184,8 +221,7 @@ object JavaServerAppPackaging {
rpmPreun <<= (rpmScriptsDirectory, rpmPreun, linuxScriptReplacements, serverLoading in Rpm, linuxJavaAppStartScriptBuilder in Rpm) apply {
(dir, preun, replacements, loader, builder) =>
Some(preun.map(_ + "\n").getOrElse("") + rpmScriptletContent(dir, Preun, loader, replacements, builder))
}
)
})
}

/* ========================================== */
Expand Down Expand Up @@ -225,4 +261,85 @@ object JavaServerAppPackaging {
val template = if (file exists) Some(file.toURI.toURL) else None
builder.generateTemplate(script, loader, replacements, template).getOrElse(sys.error("Could generate content for script: " + script))
}

/* ========================================== */
/* ===== Windows Winsw helpers Methods ====== */
/* ========================================== */

/**
* Retrieve winsw.exe and place it into windows/tmp folder.
*
* So far, we only get the exe from jenkins repository (version is fixed to the current latest version 1.16).
* Current URL is : http://repo.jenkins-ci.org/releases/com/sun/winsw/winsw/1.16/winsw-1.16-bin.exe
*
* Original name for this task is `downloadWinsw` but `download` sounds too restrictive. I (Maxence) prefer `get`.
*
*
* TODO
* We maybe want to provide mode options to get the exe
* 1. Give alternative URL to download
* 2. Embedded a tested exe into sbt-native-packager
* 3. Try to find the exe in the project (for example look in /conf or something similar)
* 4. Give custom path
*/
protected def doGetWinswExe(windowsDir: File): Option[File] = {

//QUESTION laguiz how to get streams.log here ?

//TODO laguiz we have to also package the license somewhere

//TODO laguiz give the option to provide the exe from (see scaladoc above for more details on these options)
println("DEBUG executing doGetWinswExe(tmpDir: File, normalizedProjectName: String): Option[File]")
val uri = s"http://repo.jenkins-ci.org/releases/com/sun/winsw/winsw/1.16/winsw-1.16-bin.exe" //Default URL where we fetch the bin (this could be the out-of-the-box option)
val fileToDownload = url(uri);
val binFileName = "winsw.exe";
val resultFile = windowsDir / "tmp" / "bin" / binFileName;
// if (resultFile.exists()) {
// //FIXME laguiz how to get streams.log here ?
// println("We found service exe. No need to download it again.")
// } else {
IO.download(fileToDownload, resultFile)
// }
Some(resultFile)
}

/**
* Create the Winsw XML file needed by the exe file
* Give more options to user
*/
protected def doCreateWinswXml(name: String, mainClass: Option[String], appClasspath: Seq[String] = Seq("*"), windowsDir: File): Option[File] = {

val xmlFileName = name + "_service.xml";
val resultFile = windowsDir / "tmp" / "bin" / xmlFileName;

//TODO laguiz this is specific to play and it should be somewhere else... extra options?
val noPid = scala.xml.Unparsed("""-Dpidfile.path="NUL"""")
val doubleQuote = scala.xml.Unparsed(""""""")

//Take all libraries in classpath and create the final Strings with `;` and relative path to `..\lib\`
//Here gain this is maybe only specific to Play...
val relativeAppClasspath =
appClasspath map {
(s) => """..\lib\""" + s;
} mkString (";")

//Basic arguments support
//How to provide PID no generation optional? By using a boolean ? By detecting Play project automativally?
val xml = {
<service>
<id>{ name }</id>
<name>{ name }</name>
<description>{ name }</description>
<executable>java</executable>
<arguments>-cp { doubleQuote }{ relativeAppClasspath }{ doubleQuote } { noPid } { mainClass.getOrElse("Main") }</arguments>
<logmode>rotate</logmode>
<onfailure action="restart"/>
</service>
}

IO.write(resultFile, xml.toString);

Some(resultFile)

}
}
3 changes: 3 additions & 0 deletions src/main/scala/com/typesafe/sbt/packager/windows/Keys.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ trait WindowsKeys {
val wixFile = TaskKey[File]("wix-file", "The WIX XML file to package with.")
@deprecated("use packageBin instead!", "0.7.0")
val packageMsi = TaskKey[File]("package-msi", "creates a new windows CAB file containing everything for the installation.")
val generateWinswFiles = TaskKey[(Option[File], Option[File])]("generateWinswFiles", "Creates Winsw files for Windows Services (First file is winsw exe and the second is the xml config)") //This is experimental
val getWinswExe = TaskKey[Option[File]]("getWinswExe", "Retrieve Winsw Exe and place it into /bin for Windows") //Maybe this task should be in SettingKey and/or somewhere else
val createWinswXml = TaskKey[Option[File]]("createWinswXml", "Create XML Service description used by Winsw exe.") //Maybe this task should be in SettingKey and/or somewhere else
val candleOptions = SettingKey[Seq[String]]("candle-options", "Options to pass to the candle.exe program.")
val lightOptions = SettingKey[Seq[String]]("light-options", "Options to pass to the light.exe program.")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package windows
import Keys._
import sbt._
import sbt.Keys.{ normalizedName }
import com.typesafe.sbt.packager.Keys.{ getWinswExe, createWinswXml }

trait WindowsPlugin extends Plugin {
val Windows = config("windows")
Expand Down Expand Up @@ -66,10 +67,13 @@ trait WindowsPlugin extends Plugin {
// Disable windows generation by default.
mappings := Seq.empty,
mappings in packageBin <<= mappings,
getWinswExe := None, // TODO implement how to get winswExe
createWinswXml := None,
generateWinswFiles := (getWinswExe.value, createWinswXml.value),
// TODO - Remove packageMsi after next major release.
mappings in packageMsi <<= mappings in packageBin,
packageMsi <<= packageBin,
packageBin <<= (mappings in packageMsi, wixFile, name, target, candleOptions, lightOptions, streams) map { (m, f, n, t, co, lo, s) =>
packageBin <<= (generateWinswFiles, mappings in packageMsi, wixFile, name, target, candleOptions, lightOptions, streams) map { (wf, m, f, n, t, co, lo, s) =>
val msi = t / (n + ".msi")
// First we have to move everything (including the wix file) to our target directory.
val wix = t / (n + ".wix")
Expand Down
15 changes: 12 additions & 3 deletions src/main/scala/com/typesafe/sbt/packager/windows/WixHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ case class WindowsFeature(
/** Adds a file into a given windows feature. */
case class ComponentFile(
source: String,
editable: Boolean = false) extends FeatureComponent
editable: Boolean = false,
//To avoid to duplicate a big amount of code I place this simple boolean here but we probably want more control over the service
// I'm pretty sure it would be better to create a ComponentService that would extends ComponentFile but after I don't know how to deal with it with the pattern matching vs avoid duplicate code (scala stuff that I'm not comfortable with (yet?))
isWindowsService: Boolean = false) extends FeatureComponent
/**
* Will add the directory to the windows path. NOTE: Only one of these
* per MSI.
Expand All @@ -58,7 +61,7 @@ object WixHelper {
val filenamesPrep =
for {
f <- features
ComponentFile(name, _) <- f.components
ComponentFile(name, _, _) <- f.components
} yield allParentDirs(file(name))
val filenames = filenamesPrep.flatten.map(_.toString.replaceAll("\\\\", "/")).filter(_ != "")
// Now for directories...
Expand Down Expand Up @@ -102,7 +105,7 @@ object WixHelper {
</Component>
</DirectoryRef>
ComponentInfo(id, xml)
case ComponentFile(name, editable) =>
case ComponentFile(name, editable, isWindowsService) =>
val uname = name.replaceAll("\\\\", "/")
val dir = parentDir(uname).replaceAll("//", "/").stripSuffix("/").stripSuffix("/")
val dirRef = if (dir.isEmpty) "INSTALLDIR" else cleanStringForId(dir)
Expand All @@ -121,6 +124,12 @@ object WixHelper {
} else Seq.empty
}
</File>
{
if (isWindowsService) { //So far simple config but we will give more options to control the service.
<ServiceInstall Name={ cleanFileName(fname) } ErrorControl="normal" Start="auto" Type="ownProcess" Vital="yes">
</ServiceInstall>
} else Seq.empty
}
</Component>
</DirectoryRef>
ComponentInfo(id, xml)
Expand Down