Skip to content

Commit

Permalink
Merge pull request #2718 from gatling/recorder-headless
Browse files Browse the repository at this point in the history
Recorder: implement headless frontend, close #1993
  • Loading branch information
slandelle committed May 14, 2015
2 parents 173a3b1 + 5536946 commit bacd57d
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 39 deletions.
2 changes: 2 additions & 0 deletions gatling-recorder/src/main/resources/recorder-defaults.conf
Expand Up @@ -7,6 +7,8 @@ recorder {
className = "RecordedSimulation" # The name of the generated Simulation class
thresholdForPauseCreation = 100 # The minimum time, in milliseconds, that must pass between requests to trigger a pause creation
saveConfig = false # When set to true, the configuration from the Recorder GUI overwrites this configuration
headless = false # When set to true, run the Recorder in headless mode instead of the GUI
harFilePath = "" # The path of the HAR file to convert
}
filters {
filterStrategy = "Disabled" # The selected filter resources filter strategy (currently supported : "Disabled", "BlackList", "WhiteList")
Expand Down
Expand Up @@ -18,7 +18,8 @@ package io.gatling.recorder.cli
import io.gatling.core.cli.GatlingOptionParser
import io.gatling.recorder.ConfigOverrides
import io.gatling.recorder.cli.CommandLineConstants._
import io.gatling.recorder.config.RecorderPropertiesBuilder
import io.gatling.recorder.config.{ RecorderMode, RecorderPropertiesBuilder }
import io.gatling.recorder.controller.RecorderController

private[recorder] class ArgsParser(args: Array[String]) {

Expand Down Expand Up @@ -84,6 +85,20 @@ private[recorder] class ArgsParser(args: Array[String]) {
opt[Boolean](InferHtmlResources)
.foreach(props.inferHtmlResources)
.text("""Sets the "Fetch html resources" option to true""")

opt[String](Mode)
.foreach(m => props.mode(RecorderMode(m)))
.valueName(s"<${RecorderMode.AllModes.mkString("|")}>")
.text("The Recorder mode to use")

opt[Boolean](Headless)
.foreach(props.headless)
.text("Run the Recorder in headless mode")

opt[String](HarFilePath)
.foreach(props.harFilePath)
.valueName("<HarFilePath>")
.text("The path of the HAR file to convert")
}

def parseArguments: Option[ConfigOverrides] =
Expand Down
Expand Up @@ -32,4 +32,7 @@ private[cli] object CommandLineConstants {
val FollowRedirect = CommandLineConstant("follow-redirect", "fr")
val AutomaticReferer = CommandLineConstant("automatic-referer", "ar")
val InferHtmlResources = CommandLineConstant("infer-html-resources", "ihr")
val Mode = CommandLineConstant("mode", "m")
val Headless = CommandLineConstant("headless", "cli")
val HarFilePath = CommandLineConstant("har-file", "hf")
}
Expand Up @@ -28,6 +28,8 @@ private[recorder] object ConfigKeys {
val ClassName = "recorder.core.className"
val ThresholdForPauseCreation = "recorder.core.thresholdForPauseCreation"
val SaveConfig = "recorder.core.saveConfig"
val Headless = "recorder.core.headless"
val HarFilePath = "recorder.core.harFilePath"
}
object filters {
val FilterStrategy = "recorder.filters.filterStrategy"
Expand Down
Expand Up @@ -96,15 +96,12 @@ private[recorder] object RecorderConfiguration extends StrictLogging {
configFile.foreach(file => withCloseable(createAndOpen(file).writer(gatlingConfiguration.core.charset))(_.write(configToSave.render(RenderOptions))))
}

private[config] def createAndOpen(path: Path): Path = {
private[config] def createAndOpen(path: Path): Path =
if (!path.exists) {
val parent = path.getParent
if (parent.exists) path.touch
else throw new FileNotFoundException(s"Directory '${parent.toString}' for recorder configuration does not exist")
}

path
}
} else path

private def buildConfig(config: Config): RecorderConfiguration = {
import ConfigKeys._
Expand All @@ -130,7 +127,9 @@ private[recorder] object RecorderConfiguration extends StrictLogging {
pkg = config.getString(core.Package),
className = config.getString(core.ClassName),
thresholdForPauseCreation = config.getInt(core.ThresholdForPauseCreation) milliseconds,
saveConfig = config.getBoolean(core.SaveConfig)),
saveConfig = config.getBoolean(core.SaveConfig),
headless = config.getBoolean(core.Headless),
harFilePath = config.getString(core.HarFilePath).trimToOption),
filters = FiltersConfiguration(
filterStrategy = FilterStrategy(config.getString(filters.FilterStrategy)),
whiteList = WhiteList(config.getStringList(filters.WhitelistPatterns).toList),
Expand Down Expand Up @@ -187,7 +186,9 @@ private[recorder] case class CoreConfiguration(
pkg: String,
className: String,
thresholdForPauseCreation: Duration,
saveConfig: Boolean)
saveConfig: Boolean,
headless: Boolean,
harFilePath: Option[String])

private[recorder] case class HttpConfiguration(
automaticReferer: Boolean,
Expand Down
Expand Up @@ -49,6 +49,12 @@ class RecorderPropertiesBuilder {
def saveConfig(status: Boolean): Unit =
props += core.SaveConfig -> status

def headless(status: Boolean): Unit =
props += core.Headless -> status

def harFilePath(path: String): Unit =
props += core.HarFilePath -> path

def filterStrategy(strategy: String): Unit =
props += filters.FilterStrategy -> strategy

Expand Down
Expand Up @@ -17,14 +17,14 @@ package io.gatling.recorder.ui

import io.gatling.recorder.config.{ RecorderMode, RecorderConfiguration }
import io.gatling.recorder.controller.RecorderController
import io.gatling.recorder.ui.headless.HeadlessFrontend
import io.gatling.recorder.ui.swing.SwingFrontend

private[recorder] object RecorderFrontend {

// Currently hardwired to the Swing frontend
// Will select the desired frontend when more are implemented
def newFrontend(controller: RecorderController)(implicit configuration: RecorderConfiguration): RecorderFrontend =
new SwingFrontend(controller)
if (configuration.core.headless) new HeadlessFrontend(controller)
else new SwingFrontend(controller)
}
private[recorder] abstract class RecorderFrontend(controller: RecorderController) {

Expand Down
@@ -0,0 +1,95 @@
/**
* Copyright 2011-2015 eBusiness Information, Groupe Excilys (www.ebusinessinformation.fr)
*
* 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 io.gatling.recorder.ui.headless

import java.io.{ PrintStream, File }
import java.lang.management.ManagementFactory

import io.gatling.core.util.Io._
import io.gatling.recorder.config.RecorderMode.Proxy
import io.gatling.recorder.config.RecorderConfiguration
import io.gatling.recorder.controller.RecorderController
import io.gatling.recorder.ui.{ EventInfo, RecorderFrontend }

private[headless] object HeadlessFrontend {
private val RecorderPidFile = new File(".gatling-recorder-pid")
}
private[ui] class HeadlessFrontend(controller: RecorderController)(implicit configuration: RecorderConfiguration) extends RecorderFrontend(controller) {

import HeadlessFrontend._

private var hasRun = false

override def selectedRecorderMode = configuration.core.mode

override def receiveEventInfo(eventInfo: EventInfo): Unit = println(s"[Event] $eventInfo")

override def init(): Unit =
if (!hasRun) {
hasRun = true
println("Starting Recorder in headless mode")
if (selectedRecorderMode == Proxy && RecorderPidFile.exists()) {
printErr(s"Recorder lock file found at $RecorderPidFile.")
printErr("Make sure that there is no other recording in progress.")
sys.exit(1)
} else startRecording()
} else sys.runtime.halt(0)

override def handleHarExportFailure(message: String): Unit =
printErr(s"Could not convert HAR file: $message")

override def harFilePath = configuration.core.harFilePath.getOrElse("")

override def handleHarExportSuccess(): Unit =
println("HAR file successfully converted.")

override def recordingStarted(): Unit = {
createLockFile()
println(s"Recording started, proxy port is ${configuration.proxy.port}")
println("To stop the Recorder and generate the Simulation, kill the Recorder process with: ")
println("- CTRL-C")
println(s"- Use the Recorder's PID, written to $RecorderPidFile")
sys.addShutdownHook(stopRecording(true))
}

override def handleFilterValidationFailures(failures: Seq[String]): Unit = ()

override def askSimulationOverwrite: Boolean = {
printErr("Another simulation with the same name exists.")
false
}

override def recordingStopped(): Unit = {
RecorderPidFile.delete()
println("New Gatling simulation created.")
}

override def handleMissingHarFile(path: String): Unit = {
val errorMessage = {
if (path.isEmpty) "The HAR file to convert was not specified, either through recorder.conf or through CLI options."
else s"Could not find the HAR file (path: $path)"
}
printErr(errorMessage)
}

private def createLockFile(): Unit = {
val pid = ManagementFactory.getRuntimeMXBean.getName.split("@").head
withCloseable(new PrintStream(RecorderPidFile))(_.println(pid))
}

private def printErr(msg: String): Unit =
Console.err.println(msg)
}
Expand Up @@ -480,6 +480,8 @@ private[swing] class ConfigurationFrame(frontend: RecorderFrontend)(implicit con
modeSelector.selection.item = configuration.core.mode
toggleModeSelector(modeSelector.selection.item)

configuration.core.harFilePath.foreach(harPathChooser.setPath)

localProxyHttpPort.text = configuration.proxy.port.toString

httpsModes.selection.item = configuration.proxy.https.mode
Expand All @@ -492,7 +494,7 @@ private[swing] class ConfigurationFrame(frontend: RecorderFrontend)(implicit con
certificatePathChooser.setPath(configuration.proxy.https.certificateAuthority.certificatePath)
privateKeyPathChooser.setPath(configuration.proxy.https.certificateAuthority.privateKeyPath)

configuration.proxy.outgoing.host.map { proxyHost =>
configuration.proxy.outgoing.host.foreach { proxyHost =>
outgoingProxyHost.text = proxyHost
outgoingProxyHttpPort.text = configuration.proxy.outgoing.port.map(_.toString).orNull
outgoingProxyHttpsPort.text = configuration.proxy.outgoing.sslPort.map(_.toString).orNull
Expand Down Expand Up @@ -542,6 +544,8 @@ private[swing] class ConfigurationFrame(frontend: RecorderFrontend)(implicit con

props.mode(modeSelector.selection.item)

props.harFilePath(harPathChooser.selection)

// Local proxy
props.localPort(Try(localProxyHttpPort.text.toInt).getOrElse(0))

Expand Down
84 changes: 57 additions & 27 deletions src/sphinx/http/recorder.rst
Expand Up @@ -162,40 +162,70 @@ Charles is an amazing tool and has an HAR export feature, but it's a proxy, so w

To import a HAR file, select the *HAR converter* mode in the top right dropdown in the Recorder.


.. _recorder-headless:

Headless mode
=============

Along the GUI mode, Gatling also offers a simple CLI interface, facilitating the automation of recording or converting simulations from HAR files.
The Headless mode can be enabled either from the ``recorder.conf`` file or with the ``-cli``/``--headless`` command line option.
Both 'Proxy' an 'HAR' modes are supported (you can set which mode to use using the ``-m``/``--mode`` command line option).

Proxy
-----

In 'Proxy mode', the Recorder will start listening for requests from your browser right away.
To stop the Recorder and create the Simulation, you have to 'kill' the Recorder by either:

* Sending a 'kill' signal with ``CTRL-C``
* Killing the Recorder's process, using the Recorder process ID written to the ``.gatling-recorder-pid`` file: ``cat .gatling-recorder-pid | xargs kill``

HAR Converter
-------------

In 'Har' mode, the Recorder will convert the provided HAR file to a Simulation and exits.

.. _recorder-cli:

Command-line options
====================

For those who prefer the command line, command line options can be passed to the Recorder:

+--------------------+-------------------------------------+-----------------------------------------+
| Option (short) | Option (long) | Description |
+====================+=====================================+=========================================+
| -lp <port> | --local-port <port> | Local Proxy HTTP/HTTPS port |
+--------------------+-------------------------------------+-----------------------------------------+
| -ph <port> | --proxy-host <port> | Outgoing proxy host |
+--------------------+-------------------------------------+-----------------------------------------+
| -pp <port> | --proxy-port <port> | Outgoing proxy port |
+--------------------+-------------------------------------+-----------------------------------------+
| -pps <port> | --proxy-port-ssl <port> | Outgoing proxy SSL port |
+--------------------+-------------------------------------+-----------------------------------------+
| -of <path> | --output-folder <path> | Output folder for generated simulations |
+--------------------+-------------------------------------+-----------------------------------------+
| -bdf <path> | --bodies-folder <path> | Folder for bodies |
+--------------------+-------------------------------------+-----------------------------------------+
| -cn <className> | --class-name <className> | Name of the generated simulation |
+--------------------+-------------------------------------+-----------------------------------------+
| -pkg <packageName> | --package <packageName> | Package of the generated simulation |
+--------------------+-------------------------------------+-----------------------------------------+
| -enc <encoding> | --encoding <encoding> | Encoding used in the Recorder |
+--------------------+-------------------------------------+-----------------------------------------+
| -fr <true|false> | --follow-redirect <true|false> | Enable *Follow Redirects* |
+--------------------+-------------------------------------+-----------------------------------------+
| -ar <true|false> | --automatic-referer <true|false> | Enable *Automatic Referers* |
+--------------------+-------------------------------------+-----------------------------------------+
| -fhr <true|false> | --fetch-html-resources <true|false> | Enable *Fetch html resources* |
+--------------------+-------------------------------------+-----------------------------------------+
+--------------------+-------------------------------------+------------------------------------------+
| Option (short) | Option (long) | Description |
+====================+=====================================+==========================================+
| -lp <port> | --local-port <port> | Local Proxy HTTP/HTTPS port |
+--------------------+-------------------------------------+------------------------------------------+
| -ph <port> | --proxy-host <port> | Outgoing proxy host |
+--------------------+-------------------------------------+------------------------------------------+
| -pp <port> | --proxy-port <port> | Outgoing proxy port |
+--------------------+-------------------------------------+------------------------------------------+
| -pps <port> | --proxy-port-ssl <port> | Outgoing proxy SSL port |
+--------------------+-------------------------------------+------------------------------------------+
| -of <path> | --output-folder <path> | Output folder for generated simulations |
+--------------------+-------------------------------------+------------------------------------------+
| -bdf <path> | --bodies-folder <path> | Folder for bodies |
+--------------------+-------------------------------------+------------------------------------------+
| -cn <className> | --class-name <className> | Name of the generated simulation |
+--------------------+-------------------------------------+------------------------------------------+
| -pkg <packageName> | --package <packageName> | Package of the generated simulation |
+--------------------+-------------------------------------+------------------------------------------+
| -enc <encoding> | --encoding <encoding> | Encoding used in the Recorder |
+--------------------+-------------------------------------+------------------------------------------+
| -fr <true|false> | --follow-redirect <true|false> | Enable *Follow Redirects* |
+--------------------+-------------------------------------+------------------------------------------+
| -ar <true|false> | --automatic-referer <true|false> | Enable *Automatic Referers* |
+--------------------+-------------------------------------+------------------------------------------+
| -fhr <true|false> | --fetch-html-resources <true|false> | Enable *Fetch html resources* |
+--------------------+-------------------------------------+------------------------------------------+
| -m <Proxy|Har> | --mode <Proxy|Har> | Recorder mode to use |
+--------------------+-------------------------------------+------------------------------------------+
| -cli <true|false> | --headless <true|false> | Run Recorder in headless mode |
+--------------------+-------------------------------------+------------------------------------------+
| -hf <path> | --har-file <path> | The HAR file to convert (if mode is Har) |
+--------------------+-------------------------------------+------------------------------------------+

.. note:: Command-line options override saved preferences.

Expand Down

0 comments on commit bacd57d

Please sign in to comment.