From ccdc00629b9d9c61d9d3611bf5bf96918a5cdfaf Mon Sep 17 00:00:00 2001 From: Florian Schmaus <flow@cs.fau.de> Date: Tue, 19 Apr 2022 11:08:13 +0200 Subject: [PATCH] Add XmppHook --- benchmark-runner/build.sc | 4 + .../main/src/de/fau/cs/mazstab/Mazstab.scala | 10 +- .../fau/cs/mazstab/MazstabConfiguration.scala | 22 ++- .../de/fau/cs/mazstab/hooks/MazstabHook.scala | 11 +- .../de/fau/cs/mazstab/hooks/XmppHook.scala | 165 ++++++++++++++++++ 5 files changed, 203 insertions(+), 9 deletions(-) create mode 100644 benchmark-runner/main/src/de/fau/cs/mazstab/hooks/XmppHook.scala diff --git a/benchmark-runner/build.sc b/benchmark-runner/build.sc index 0b27f14..11edc82 100644 --- a/benchmark-runner/build.sc +++ b/benchmark-runner/build.sc @@ -14,6 +14,8 @@ object main extends ScalaModule def mainClass = Some("de.fau.cs.mazstab.Mazstab") def scalaVersion = "2.13.5" + + val smackVersion = "4.4.5" def ivyDeps = Agg( ivy"com.google.guava:guava:30.1.1-jre", ivy"com.jakewharton.picnic:picnic:0.5.0", @@ -28,6 +30,8 @@ object main extends ScalaModule ivy"org.apache.commons:commons-configuration2:2.7", ivy"org.apache.commons:commons-math3:3.6.1", ivy"org.eclipse.jgit:org.eclipse.jgit:5.13.0.202109080827-r", + ivy"org.igniterealtime.smack:smack-java8:$smackVersion", + ivy"org.igniterealtime.smack:smack-tcp:$smackVersion", ivy"org.rogach::scallop:4.1.0", ivy"org.scala-lang.modules::scala-collection-contrib:0.2.2", ivy"org.slf4j:slf4j-jdk14:1.7.32", diff --git a/benchmark-runner/main/src/de/fau/cs/mazstab/Mazstab.scala b/benchmark-runner/main/src/de/fau/cs/mazstab/Mazstab.scala index 9745f52..b087481 100644 --- a/benchmark-runner/main/src/de/fau/cs/mazstab/Mazstab.scala +++ b/benchmark-runner/main/src/de/fau/cs/mazstab/Mazstab.scala @@ -86,11 +86,13 @@ class Mazstab(val context: MazstabContext) extends AutoCloseable { @SuppressWarnings(Array("scalafix:DisableSyntax.var")) private var throwable: Option[Throwable] = None + def getThrowable = throwable val hooks: Seq[MazstabHook] = { import de.fau.cs.mazstab.hooks._ List( - new UserDirectoryHook(this) + new UserDirectoryHook(this), + new XmppHook(this), ) } @@ -477,6 +479,8 @@ class Mazstab(val context: MazstabContext) extends AutoCloseable { errLogFileStream.close() deleteIfExistsAndEmpty(errLogFile) + + for (h <- hooks) h.close() } // TODO: Once https://github.com/com-lihaoyi/os-lib/pull/100 is @@ -549,6 +553,10 @@ class Mazstab(val context: MazstabContext) extends AutoCloseable { val NEWLINE, NO_NEWLINE = Value } + def logv(verboseMessage: String) = if (conf.isVerbose) log(verboseMessage) + + def logd(debugMessage: String) = if (conf.isDebug) log(debugMessage) + def log( message: String, logOption: LogOption.Value = LogOption.NEWLINE, diff --git a/benchmark-runner/main/src/de/fau/cs/mazstab/MazstabConfiguration.scala b/benchmark-runner/main/src/de/fau/cs/mazstab/MazstabConfiguration.scala index ce6050e..1188edd 100644 --- a/benchmark-runner/main/src/de/fau/cs/mazstab/MazstabConfiguration.scala +++ b/benchmark-runner/main/src/de/fau/cs/mazstab/MazstabConfiguration.scala @@ -63,10 +63,12 @@ case class MazstabUserConfiguration( outDirectory: Path, myDomains: Option[List[String]], debugRoot: Option[Path], + xmppNotification: Option[de.fau.cs.mazstab.hooks.XmppHookUserConfiguration], ) object MazstabConfigurationYamlProtocol extends DefaultYamlProtocol { - implicit val mazstabUserConfigurationFormat = yamlFormat3( + import de.fau.cs.mazstab.hooks.XmppHookUserConfigurationProtocol._ + implicit val mazstabUserConfigurationFormat = yamlFormat4( MazstabUserConfiguration ) } @@ -99,7 +101,7 @@ object MazstabConfiguration { val xdgConfigDirectory = os.Path(mazstabXdgProjectDirectories.configDir) - def getUserConfiguration(): Option[MazstabUserConfiguration] = { + lazy val userConfiguration: Option[MazstabUserConfiguration] = { val userConfigurationYaml = xdgConfigDirectory / "config.yml" if (userConfigurationYaml.toIO.isFile) { @@ -114,13 +116,13 @@ object MazstabConfiguration { } } - lazy val debugRoot: os.Path = getUserConfiguration() + lazy val debugRoot: os.Path = userConfiguration .map(_.debugRoot) .flatten .getOrElse(pathOf("~/repos/uni/mazstab")) def getOutDirectory(mazstabRootDirectory: Path): Path = - getUserConfiguration() map { + userConfiguration map { _.outDirectory } getOrElse (mazstabRootDirectory / "out") @@ -140,7 +142,7 @@ object MazstabConfiguration { if (nameComponents.length == 1) { dnsName } else { val domainpart = nameComponents.drop(1).mkString(".") - val shorten = getUserConfiguration() map { + val shorten = userConfiguration map { _.myDomains } map { _.contains(domainpart) @@ -154,7 +156,7 @@ object MazstabConfiguration { } } - lazy val hostNameForExperimentDirectory: String = { + lazy val mazstabHostname: String = { val canonicalHostName = java.net.InetAddress.getLocalHost().getCanonicalHostName() maybeShortenDnsName(canonicalHostName) @@ -172,7 +174,7 @@ class MazstabConfiguration(arguments: Seq[String]) lazy val defaultHostExperimentDirectory = MazstabConfiguration.getOutDirectory( rootDirectory() - ) / MazstabConfiguration.hostNameForExperimentDirectory + ) / MazstabConfiguration.mazstabHostname lazy val defaultExperimentDirectory = defaultHostExperimentDirectory / Mazstab .getTimestamp() @@ -307,6 +309,12 @@ class MazstabConfiguration(arguments: Seq[String]) default = Some(false) ) + val verbose = tally() + + def isVerbose = verbose() > 0 + + def isDebug = verbose() > 1 + val verifyResult = opt[VerifyResult.Value]( default = Some(if (extended()) VerifyResult.always else VerifyResult.once), descr = diff --git a/benchmark-runner/main/src/de/fau/cs/mazstab/hooks/MazstabHook.scala b/benchmark-runner/main/src/de/fau/cs/mazstab/hooks/MazstabHook.scala index 247055f..2b0114e 100644 --- a/benchmark-runner/main/src/de/fau/cs/mazstab/hooks/MazstabHook.scala +++ b/benchmark-runner/main/src/de/fau/cs/mazstab/hooks/MazstabHook.scala @@ -2,10 +2,19 @@ // Copyright © 2022 Florian Schmaus package de.fau.cs.mazstab.hooks -class PreHookFailure {} +class PreHookFailure + +object ThrowablePreHookFailure { + def apply(throwable: Throwable) = new ThrowablePreHookFailure(throwable) +} + +class ThrowablePreHookFailure(val throwable: Throwable) extends PreHookFailure { + override def toString = throwable.toString() +} abstract class MazstabHook { // Default hooks do nothing. def preHook(): Option[PreHookFailure] = None def postHook(): Unit = {} + def close(): Unit = {} } diff --git a/benchmark-runner/main/src/de/fau/cs/mazstab/hooks/XmppHook.scala b/benchmark-runner/main/src/de/fau/cs/mazstab/hooks/XmppHook.scala new file mode 100644 index 0000000..b37e763 --- /dev/null +++ b/benchmark-runner/main/src/de/fau/cs/mazstab/hooks/XmppHook.scala @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright © 2022 Florian Schmaus +package de.fau.cs.mazstab.hooks + +import scala.util._ + +import de.fau.cs.mazstab.Mazstab +import de.fau.cs.mazstab.MazstabConfiguration + +import net.jcazevedo.moultingyaml._ + +import org.jxmpp.jid.EntityBareJid +import org.jivesoftware.smack.tcp.XMPPTCPConnection +import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration + +object XmppHookUserConfigurationProtocol extends DefaultYamlProtocol { + implicit object EntityBareJidFormat extends YamlFormat[EntityBareJid] { + override def write(jid: EntityBareJid) = YamlString(jid.toString) + override def read(value: YamlValue) = value match { + case YamlString(s) => + Try(org.jxmpp.jid.impl.JidCreate.entityBareFrom(s)) match { + case Success(jid) => jid + case Failure(throwable) => + deserializationError(s"'$s' is not a valid JID", throwable) + } + case other => deserializationError(s"Invalid entity bare JID: $other") + } + } + + implicit val xmppHookUserConfiguration = yamlFormat3( + XmppHookUserConfiguration + ) +} + +case class XmppHookUserConfiguration( + jid: EntityBareJid, + password: String, + recipient: EntityBareJid, +) + +class XmppHook(mazstab: Mazstab) extends MazstabHook { + import java.util.concurrent.{Semaphore, TimeUnit} + import scala.concurrent._ + import ExecutionContext.Implicits.global + + val timeout = duration.Duration(300, duration.MILLISECONDS) + val initialExtraTimeout = duration.Duration(1000, duration.MILLISECONDS) + + def hookConfigOption: Option[XmppHookUserConfiguration] = + MazstabConfiguration.userConfiguration.map(_.xmppNotification).flatten + + val connectionOption = hookConfigOption map { hookConfig => + Future { + val xmppConnectionConfig = XMPPTCPConnectionConfiguration + .builder() + .setXmppAddressAndPassword(hookConfig.jid, hookConfig.password) + .build() + + val connection = new XMPPTCPConnection(xmppConnectionConfig) + blocking { + connection.connect().login() + } + + mazstab.logv(s"$connection established") + + connection + } + } + + private def sendMessage( + connection: XMPPTCPConnection, + subject: String, + message: String, + ) = { + val recipient = hookConfigOption.get.recipient + val stanza = connection + .getStanzaFactory() + .buildMessageStanza() + .ofType(org.jivesoftware.smack.packet.Message.Type.headline) + .to(recipient) + .setSubject(subject) + .setBody(message) + .build() + + val ackListenerAndSemOption = if (connection.isSmEnabled()) { + val ackSem = new Semaphore(0) + val ackListener = new org.jivesoftware.smack.StanzaListener() { + override def processStanza( + stanza: org.jivesoftware.smack.packet.Stanza + ) = ackSem.release() + } + connection.addStanzaIdAcknowledgedListener( + stanza.getStanzaId(), + ackListener, + ) + Some(ackListener, ackSem) + } else None + + mazstab.logd(s"Sending XMPP message to $recipient") + + try { + connection.sendStanza(stanza) + + ackListenerAndSemOption map { ackListenerAndSem => + { + connection.requestSmAcknowledgement() + + ackListenerAndSem._2.tryAcquire( + timeout.toMillis, + TimeUnit.MILLISECONDS, + ) + } + } + } finally { + ackListenerAndSemOption map { ackListenerAndSem => + connection.removeStanzaAcknowledgedListener(ackListenerAndSem._1) + } + } + } + + override def preHook(): Option[PreHookFailure] = + connectionOption flatMap { connectionFuture => + { + mazstab.log("Preparing to send pre-hook notification via XMPP") + val hostname = MazstabConfiguration.mazstabHostname + + val subject = s"Mazstab run started on $hostname" + val message = s"$subject at ${Mazstab.getTimestamp()}" + + import de.fau.cs.mazstab.hooks.ThrowablePreHookFailure + + Try( + Await.result(connectionFuture, timeout + initialExtraTimeout) + ) match { + case Success(connection) => { + sendMessage(connection, subject, message) + None + } + case Failure(throwable) => + Some(ThrowablePreHookFailure(throwable)) + } + } + } + + override def postHook() = connectionOption map { connectionFuture => + { + val hostname = MazstabConfiguration.mazstabHostname + val throwable = mazstab.getThrowable + val status = if (throwable.isDefined) "failed" else "successful" + + val subject = s"Mazstab run $status on $hostname" + val message = s"$subject. Took ${mazstab.context.getPrettyPrintedAge()}" + + Try(Await.result(connectionFuture, timeout)) + .map(connection => sendMessage(connection, subject, message)) + } + } + + override def close() = connectionOption map { connectionFuture => + Try( + Await + .result(connectionFuture, timeout) + ).map(connection => connection.disconnect) + } +} -- GitLab