Skip to content

Commit

Permalink
[JENKINS-41516] Add script console listener (alternative/extended pro…
Browse files Browse the repository at this point in the history
…posal) (#7056)

* Set location for listener

* Add ScriptListener

* Remove debugging statements

* Add listener for CLI

* Add new listener for other sources

* Simplify listener

* Fix whitespace and comments

* Update core/src/main/java/hudson/cli/GroovyCommand.java

Co-authored-by: Wadeck Follonier <[email protected]>

* Add precise origins; Correctly log CLI input

* Add LoggingGroovySh as suggested by @daniel-beck

* Add ScriptListener Tests

* Add basic default listener that just logs

* WIP Script Listener

* Cleaner ScriptListener API

* Add license

* Cleaner Binding stringification

* Note TODO

* Add Object feature parameters

* Thanks Checkstyle

* Thanks Spotbugs

* Simplify test, make it pass again

* Add since TODO

* No mocking, use CLICommandInvoker for CLI commands

* Minor cleanup

* Fix imports

* So far, #onScriptDefinition would be unused, so remove it

* Checkstyle

* Add test for DefaultScriptListener

* Nicer message for the (hopefully rare) case of anon script execution

* More thoroughly test logging

* Remove obsolete TODO

---------

Co-authored-by: Meiswinkel, Jan  SF/HZA-ZC2S <[email protected]>
Co-authored-by: Jan Meiswinkel <[email protected]>
Co-authored-by: meiswjn <[email protected]>
Co-authored-by: Daniel Beck <[email protected]>
Co-authored-by: Wadeck Follonier <[email protected]>
Co-authored-by: Basil Crow <[email protected]>
  • Loading branch information
7 people authored Oct 5, 2023
1 parent 32834c5 commit 270062a
Show file tree
Hide file tree
Showing 7 changed files with 511 additions and 6 deletions.
10 changes: 8 additions & 2 deletions core/src/main/java/hudson/cli/GroovyCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import hudson.Extension;
import hudson.model.User;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import jenkins.model.Jenkins;
import jenkins.util.ScriptListener;
import org.apache.commons.io.IOUtils;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
Expand Down Expand Up @@ -63,14 +65,18 @@ protected int run() throws Exception {
// this allows the caller to manipulate the JVM state, so require the execute script privilege.
Jenkins.get().checkPermission(Jenkins.ADMINISTER);

final String scriptListenerCorrelationId = String.valueOf(System.identityHashCode(this));

Binding binding = new Binding();
binding.setProperty("out", new PrintWriter(new OutputStreamWriter(stdout, getClientCharset()), true));
binding.setProperty("out", new ScriptListener.ListenerWriter(new PrintWriter(new OutputStreamWriter(stdout, getClientCharset()), true), GroovyCommand.class, null, scriptListenerCorrelationId, User.current()));
binding.setProperty("stdin", stdin);
binding.setProperty("stdout", stdout);
binding.setProperty("stderr", stderr);

GroovyShell groovy = new GroovyShell(Jenkins.get().getPluginManager().uberClassLoader, binding);
groovy.run(loadScript(), "RemoteClass", remaining.toArray(new String[0]));
String script = loadScript();
ScriptListener.fireScriptExecution(script, binding, GroovyCommand.class, null, scriptListenerCorrelationId, User.current());
groovy.run(script, "RemoteClass", remaining.toArray(new String[0]));
return 0;
}

Expand Down
30 changes: 27 additions & 3 deletions core/src/main/java/hudson/cli/GroovyshCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import groovy.lang.Binding;
import groovy.lang.Closure;
import hudson.Extension;
import hudson.model.User;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
Expand All @@ -39,6 +40,7 @@
import java.util.ArrayList;
import java.util.List;
import jenkins.model.Jenkins;
import jenkins.util.ScriptListener;
import jline.TerminalFactory;
import jline.UnsupportedTerminal;
import org.codehaus.groovy.tools.shell.Groovysh;
Expand All @@ -54,6 +56,9 @@
*/
@Extension
public class GroovyshCommand extends CLICommand {

private final String scriptListenerCorrelationId = String.valueOf(System.identityHashCode(this));

@Override
public String getShortDescription() {
return Messages.GroovyshCommand_ShortDescription();
Expand All @@ -78,6 +83,8 @@ protected int run() {
commandLine.append(arg);
}

// TODO Add binding
ScriptListener.fireScriptExecution(null, null, GroovyshCommand.class, null, scriptListenerCorrelationId, User.current());
Groovysh shell = createShell(stdin, stdout, stderr);
return shell.run(commandLine.toString());
}
Expand All @@ -96,11 +103,14 @@ protected Groovysh createShell(InputStream stdin, PrintStream stdout,
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
binding.setProperty("out", new PrintWriter(new OutputStreamWriter(stdout, charset), true));

binding.setProperty("out", new PrintWriter(new OutputStreamWriter(new ScriptListener.ListenerOutputStream(stdout, charset, GroovyshCommand.class, null, scriptListenerCorrelationId, User.current()), charset), true));
binding.setProperty("hudson", Jenkins.get()); // backward compatibility
binding.setProperty("jenkins", Jenkins.get());

IO io = new IO(new BufferedInputStream(stdin), stdout, stderr);
IO io = new IO(new BufferedInputStream(stdin),
new ScriptListener.ListenerOutputStream(stdout, charset, GroovyshCommand.class, null, scriptListenerCorrelationId, User.current()),
new ScriptListener.ListenerOutputStream(stderr, charset, GroovyshCommand.class, null, scriptListenerCorrelationId, User.current()));

final ClassLoader cl = Jenkins.get().pluginManager.uberClassLoader;
Closure registrar = new Closure(null, null) {
Expand All @@ -119,9 +129,23 @@ public Object doCall(Object[] args) {
return null;
}
};
Groovysh shell = new Groovysh(cl, binding, io, registrar);
Groovysh shell = new LoggingGroovySh(cl, binding, io, registrar);
shell.getImports().add("hudson.model.*");
return shell;
}

private class LoggingGroovySh extends Groovysh {
private final Binding binding;

LoggingGroovySh(ClassLoader cl, Binding binding, IO io, Closure registrar) {
super(cl, binding, io, registrar);
this.binding = binding;
}

@Override
protected void maybeRecordInput(String line) {
ScriptListener.fireScriptExecution(line, binding, GroovyshCommand.class, null, scriptListenerCorrelationId, User.current());
super.maybeRecordInput(line);
}
}
}
10 changes: 9 additions & 1 deletion core/src/main/java/hudson/util/RemotingDiagnostics.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import hudson.FilePath;
import hudson.Functions;
import hudson.Util;
import hudson.model.User;
import hudson.remoting.AsyncFutureImpl;
import hudson.remoting.DelegatingCallable;
import hudson.remoting.Future;
Expand All @@ -46,11 +47,13 @@
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import javax.management.JMException;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import jenkins.model.Jenkins;
import jenkins.security.MasterToSlaveCallable;
import jenkins.util.ScriptListener;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.customizers.ImportCustomizer;
import org.kohsuke.stapler.StaplerRequest;
Expand Down Expand Up @@ -112,7 +115,12 @@ public Map<String, String> call() {
* Executes Groovy script remotely.
*/
public static String executeGroovy(String script, @NonNull VirtualChannel channel) throws IOException, InterruptedException {
return channel.call(new Script(script));
final String correlationId = UUID.randomUUID().toString();
final String context = channel.toString();
ScriptListener.fireScriptExecution(script, new Binding(), RemotingDiagnostics.class, context, correlationId, User.current());
final String output = channel.call(new Script(script));
ScriptListener.fireScriptOutput(output, RemotingDiagnostics.class, context, correlationId, User.current());
return output;
}

private static final class Script extends MasterToSlaveCallable<String, RuntimeException> implements DelegatingCallable<String, RuntimeException> {
Expand Down
67 changes: 67 additions & 0 deletions core/src/main/java/jenkins/util/DefaultScriptListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* The MIT License
*
* Copyright (c) 2022 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package jenkins.util;

import edu.umd.cs.findbugs.annotations.NonNull;
import groovy.lang.Binding;
import hudson.Extension;
import hudson.model.User;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

/**
* Basic default implementation of {@link jenkins.util.ScriptListener} that just logs.
*
* @since TODO
*/
@Extension
@Restricted(NoExternalUse.class)
public class DefaultScriptListener implements ScriptListener {
public static final Logger LOGGER = Logger.getLogger(DefaultScriptListener.class.getName());

@Override
public void onScriptExecution(String script, Binding binding, @NonNull Object feature, Object context, @NonNull String correlationId, User user) {
String userFragment = user == null ? " (no user)" : " by user: '" + user + "'";
LOGGER.log(Level.FINE, LOGGER.isLoggable(Level.FINEST) ? new Exception() : null,
() -> "Execution of script: '" + script + "' with binding: '" + stringifyBinding(binding) + "' in feature: '" + feature + "' and context: '" + context + "' with correlation: '" + correlationId + "'" + userFragment);
}

@Override
public void onScriptOutput(String output, @NonNull Object feature, Object context, @NonNull String correlationId, User user) {
String userFragment = user == null ? " (no user)" : " for user: '" + user + "'";
LOGGER.log(Level.FINER, LOGGER.isLoggable(Level.FINEST) ? new Exception() : null,
() -> "Script output: '" + output + "' in feature: '" + feature + "' and context: '" + context + "' with correlation: '" + correlationId + "'" + userFragment);
}

private static String stringifyBinding(Binding binding) {
if (binding == null) {
return null;
}
return InvokerHelper.toString(binding.getVariables());
}
}
Loading

0 comments on commit 270062a

Please sign in to comment.