@Override
  public Properties getEnvironmentVariables(Platform platform, OptionsByType optionsByType) {
    Table diagnosticsTable = optionsByType.get(Table.class);
    EnvironmentVariables environmentVariables =
        optionsByType.getOrSetDefault(
            EnvironmentVariables.class,
            EnvironmentVariables.of(EnvironmentVariables.Source.TargetPlatform));

    Properties variables = new Properties();

    switch (environmentVariables.getSource()) {
      case Custom:
        if (diagnosticsTable != null) {
          diagnosticsTable.addRow("Environment Variables", "(cleared)");
        }

        break;

      case ThisApplication:
        variables.putAll(System.getenv());

        if (diagnosticsTable != null) {
          diagnosticsTable.addRow("Environment Variables", "(based on parent process)");
        }

        break;

      case TargetPlatform:
        if (diagnosticsTable != null) {
          diagnosticsTable.addRow("Environment Variables", "(based on platform defaults)");
        }

        break;
    }

    // add the optionally defined environment variables
    variables.putAll(environmentVariables.realize(platform, optionsByType.asArray()));

    if (variables.size() > 0 && diagnosticsTable != null) {
      Table table = Tabularize.tabularize(variables);

      diagnosticsTable.addRow("", table.toString());
    }

    return variables;
  }
  @Override
  public A launch(Platform platform, MetaClass<A> metaClass, OptionsByType optionsByType) {
    // establish the diagnostics output table
    Table diagnosticsTable = new Table();

    diagnosticsTable.getOptions().add(Table.orderByColumn(0));

    if (platform != null) {
      diagnosticsTable.addRow("Target Platform", platform.getName());
    }

    // ----- establish the launch Options for the Application -----

    // add the platform options
    OptionsByType launchOptions = OptionsByType.of(platform.getOptions()).addAll(optionsByType);

    // add the meta-class options
    metaClass.onLaunching(platform, launchOptions);

    // ---- establish the default Options ----

    // define the PlatformSeparators as Unix if they are not already defined
    launchOptions.addIfAbsent(PlatformSeparators.forUnix());

    // define the default Platform Shell (assume BASH)
    launchOptions.addIfAbsent(Shell.is(Shell.Type.BASH));

    // define the "local.address" variable so that is can be used for resolving this platform
    // address
    launchOptions.add(
        Variable.with("local.address", LocalPlatform.get().getAddress().getHostAddress()));

    // ----- establish an identity for the application -----

    // add a unique runtime id for expression support
    launchOptions.add(Variable.with("bedrock.runtime.id", UUID.randomUUID()));

    // ----- establish default Profiles for this Platform (and Builder) -----

    // auto-detect and add externally defined profiles
    launchOptions.addAll(Profiles.getProfiles());

    // ----- notify the Profiles that the application is about to be launched -----

    for (Profile profile : launchOptions.getInstancesOf(Profile.class)) {
      profile.onLaunching(platform, metaClass, launchOptions);
    }

    // ----- add the diagnostic table to the options so it can be used by the terminal -----
    launchOptions.add(diagnosticsTable);

    // ----- prior to launching the application, let the implementation enhance the launch options
    // -----

    onLaunching(launchOptions);

    // ----- give the MetaClass a last chance to manipulate any options -----

    metaClass.onLaunch(platform, launchOptions);

    // ----- determine the display name for the application -----

    DisplayName displayName = getDisplayName(launchOptions);

    // determine the Executable
    Executable executable = launchOptions.get(Executable.class);

    // ----- deploy remote application artifacts -----

    // determine the DeploymentArtifacts based on those specified by the Deployment option
    ArrayList<DeploymentArtifact> artifactsToDeploy = new ArrayList<>();
    Deployment deployment = launchOptions.get(Deployment.class);

    if (deployment != null) {
      try {
        artifactsToDeploy.addAll(deployment.getDeploymentArtifacts(platform, launchOptions));
      } catch (Exception e) {
        throw new RuntimeException("Failed to determine artifacts to deploy", e);
      }
    }

    // determine the separators for the platform
    PlatformSeparators separators = launchOptions.get(PlatformSeparators.class);

    // assume the remote directory is the working directory
    WorkingDirectory workingDirectory =
        launchOptions.getOrSetDefault(
            WorkingDirectory.class, WorkingDirectory.temporaryDirectory());
    File remoteDirectoryFile = workingDirectory.resolve(platform, launchOptions);

    if (remoteDirectoryFile == null) {
      remoteDirectoryFile = WorkingDirectory.temporaryDirectory().resolve(platform, launchOptions);
    }

    String remoteDirectory = separators.asPlatformFileName(remoteDirectoryFile.toString());

    // Set the resolved working directory back into the options
    launchOptions.add(WorkingDirectory.at(remoteDirectoryFile));

    if (remoteDirectoryFile != null) {
      diagnosticsTable.addRow("Working Directory", remoteDirectoryFile.toString());
    }

    // Obtain the RemoteShell that will be used to launch the process
    RemoteTerminalBuilder terminalBuilder =
        launchOptions.getOrSetDefault(RemoteTerminalBuilder.class, RemoteTerminals.ssh());
    RemoteTerminal terminal = terminalBuilder.build(platform);

    // create the working directory
    terminal.makeDirectories(remoteDirectory, launchOptions);

    // deploy any artifacts required
    Deployer deployer = launchOptions.getOrSetDefault(Deployer.class, new SftpDeployer());

    DeployedArtifacts deployedArtifacts =
        deployer.deploy(artifactsToDeploy, remoteDirectory, platform, launchOptions.asArray());

    // add the remote directory as something to clean up
    deployedArtifacts.add(remoteDirectoryFile);

    if (!deployedArtifacts.isEmpty()) {
      // when we've deployed artifacts we need to add a listener to clean them up
      launchOptions.add(
          Decoration.of(
              new ApplicationListener<A>() {
                @Override
                public void onClosing(A application, OptionsByType optionsByType) {
                  // nothing to do on closing
                }

                @Override
                public void onClosed(A application, OptionsByType optionsByType) {
                  // undeploy the deployed artifacts
                  deployer.undeploy(deployedArtifacts, platform, launchOptions.asArray());
                }

                @Override
                public void onLaunched(A application) {
                  // nothing to do after launching
                }
              }));
    }

    // Realize the application arguments
    Arguments arguments = launchOptions.get(Arguments.class);
    List<String> argList = arguments.resolve(platform, launchOptions);

    // Set the actual arguments used back into the options
    launchOptions.add(Arguments.of(argList));

    // TODO: put a try/catch around the terminal.launch here so we can clean up the RemoteExecutor
    // if
    // the application failed to launch

    // determine the application class that will represent the running application
    Class<? extends A> applicationClass = metaClass.getImplementationClass(platform, launchOptions);

    diagnosticsTable.addRow("Application", displayName.resolve(launchOptions));

    if (argList.size() > 0) {
      diagnosticsTable.addRow(
          "Application Arguments ", argList.stream().collect(Collectors.joining(" ")));
    }

    diagnosticsTable.addRow(
        "Application Launch Time", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

    // ----- start the process and establish the application -----

    // launch the remote process
    RemoteApplicationProcess remoteProcess = terminal.launch(this, applicationClass, launchOptions);

    // adapt the remote process into something that the application can use
    ApplicationProcess process = adapt(remoteProcess);

    // create the Application based on the RemoteApplicationProcess
    A application;

    try {
      // attempt to find a constructor(Platform, JavaApplicationProcess, Options)
      Constructor<? extends A> constructor =
          ReflectionHelper.getCompatibleConstructor(
              applicationClass, platform.getClass(), process.getClass(), OptionsByType.class);

      // create the application
      application = constructor.newInstance(platform, process, launchOptions);
    } catch (Exception e) {
      throw new RuntimeException(
          "Failed to instantiate the Application class specified by the MetaClass:" + metaClass, e);
    }

    // ----- after launching the application, let the implementation interact with the application
    // -----

    onLaunched(application, launchOptions);

    // ----- notify the MetaClass that the application has been launched -----

    metaClass.onLaunched(platform, application, launchOptions);

    // ----- notify the Profiles that the application has been launched -----

    for (Profile profile : launchOptions.getInstancesOf(Profile.class)) {
      profile.onLaunched(platform, application, launchOptions);
    }

    // ----- notify all of the application listeners -----

    // notify the ApplicationListener-based Options that the application has been launched
    for (ApplicationListener listener : launchOptions.getInstancesOf(ApplicationListener.class)) {
      listener.onLaunched(application);
    }

    return application;
  }