Skip to content

Commit

Permalink
[MJARSIGNER-72] Parallel signing for increased speed (#18)
Browse files Browse the repository at this point in the history
Adding support for threadCount when signing jar files
  • Loading branch information
schedin authored Dec 15, 2023
1 parent 7e47f46 commit 930b7e4
Show file tree
Hide file tree
Showing 6 changed files with 419 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.ResourceBundle;

import org.apache.maven.artifact.Artifact;
Expand Down Expand Up @@ -279,65 +280,70 @@ public final void execute() throws MojoExecutionException {
jarSigner.setToolchain(toolchain);
}

int processed = 0;
List<File> archives = findJarfiles();
processArchives(archives);
getLog().info(getMessage("processed", archives.size()));
}

/**
* Finds all jar files, by looking at the Maven project and user configuration.
*
* @return a List of File objects
* @throws MojoExecutionException if it was not possible to build a list of jar files
*/
private List<File> findJarfiles() throws MojoExecutionException {
if (this.archive != null) {
processArchive(this.archive);
processed++;
} else {
if (processMainArtifact) {
processed += processArtifact(this.project.getArtifact()) ? 1 : 0;
}
// Only process this, but nothing more
return Arrays.asList(this.archive);
}

if (processAttachedArtifacts) {
Collection<String> includes = new HashSet<>();
if (includeClassifiers != null) {
includes.addAll(Arrays.asList(includeClassifiers));
}
List<File> archives = new ArrayList<>();
if (processMainArtifact) {
getFileFromArtifact(this.project.getArtifact()).ifPresent(archives::add);
}

Collection<String> excludes = new HashSet<>();
if (excludeClassifiers != null) {
excludes.addAll(Arrays.asList(excludeClassifiers));
}
if (processAttachedArtifacts) {
Collection<String> includes = new HashSet<>();
if (includeClassifiers != null) {
includes.addAll(Arrays.asList(includeClassifiers));
}

for (Artifact artifact : this.project.getAttachedArtifacts()) {
if (!includes.isEmpty() && !includes.contains(artifact.getClassifier())) {
continue;
}
Collection<String> excludes = new HashSet<>();
if (excludeClassifiers != null) {
excludes.addAll(Arrays.asList(excludeClassifiers));
}

if (excludes.contains(artifact.getClassifier())) {
continue;
}
for (Artifact artifact : this.project.getAttachedArtifacts()) {
if (!includes.isEmpty() && !includes.contains(artifact.getClassifier())) {
continue;
}

processed += processArtifact(artifact) ? 1 : 0;
if (excludes.contains(artifact.getClassifier())) {
continue;
}

getFileFromArtifact(artifact).ifPresent(archives::add);
}
} else {
if (verbose) {
getLog().info(getMessage("ignoringAttachments"));
} else {
if (verbose) {
getLog().info(getMessage("ignoringAttachments"));
} else {
getLog().debug(getMessage("ignoringAttachments"));
}
getLog().debug(getMessage("ignoringAttachments"));
}
}

if (archiveDirectory != null) {
String includeList = (includes != null) ? StringUtils.join(includes, ",") : null;
String excludeList = (excludes != null) ? StringUtils.join(excludes, ",") : null;

List<File> jarFiles;
try {
jarFiles = FileUtils.getFiles(archiveDirectory, includeList, excludeList);
} catch (IOException e) {
throw new MojoExecutionException("Failed to scan archive directory for JARs: " + e.getMessage(), e);
}
if (archiveDirectory != null) {
String includeList = (includes != null) ? StringUtils.join(includes, ",") : null;
String excludeList = (excludes != null) ? StringUtils.join(excludes, ",") : null;

for (File jarFile : jarFiles) {
processArchive(jarFile);
processed++;
}
try {
archives.addAll(FileUtils.getFiles(archiveDirectory, includeList, excludeList));
} catch (IOException e) {
throw new MojoExecutionException("Failed to scan archive directory for JARs: " + e.getMessage(), e);
}
}

getLog().info(getMessage("processed", processed));
return archives;
}

/**
Expand All @@ -358,7 +364,7 @@ public final void execute() throws MojoExecutionException {
*
* @param commandLine The {@code Commandline} to get a string representation of.
* @return The string representation of {@code commandLine}.
* @throws NullPointerException if {@code commandLine} is {@code null}.
* @throws NullPointerException if {@code commandLine} is {@code null}
*/
protected String getCommandlineInfo(final Commandline commandLine) {
if (commandLine == null) {
Expand All @@ -384,45 +390,39 @@ public String getStorepass() {
* @param artifact The artifact to check, may be <code>null</code>.
* @return <code>true</code> if the artifact looks like a ZIP file, <code>false</code> otherwise.
*/
private boolean isZipFile(final Artifact artifact) {
private static boolean isZipFile(final Artifact artifact) {
return artifact != null && artifact.getFile() != null && JarSignerUtil.isZipFile(artifact.getFile());
}

/**
* Processes a given artifact.
* Examines an Artifact and extract the File object pointing to the Artifact jar file.
*
* @param artifact The artifact to process.
* @return <code>true</code> if the artifact is a JAR and was processed, <code>false</code> otherwise.
* @throws NullPointerException if {@code artifact} is {@code null}.
* @throws MojoExecutionException if processing {@code artifact} fails.
* @param artifact the artifact to examine
* @return An Optional containing the File, or Optional.empty() if the File is not a jar file.
* @throws NullPointerException if {@code artifact} is {@code null}
*/
private boolean processArtifact(final Artifact artifact) throws MojoExecutionException {
private Optional<File> getFileFromArtifact(final Artifact artifact) {
if (artifact == null) {
throw new NullPointerException("artifact");
}

boolean processed = false;

if (isZipFile(artifact)) {
processArchive(artifact.getFile());

processed = true;
} else {
if (this.verbose) {
getLog().info(getMessage("unsupported", artifact));
} else if (getLog().isDebugEnabled()) {
getLog().debug(getMessage("unsupported", artifact));
}
return Optional.of(artifact.getFile());
}

return processed;
if (this.verbose) {
getLog().info(getMessage("unsupported", artifact));
} else if (getLog().isDebugEnabled()) {
getLog().debug(getMessage("unsupported", artifact));
}
return Optional.empty();
}

/**
* Pre-processes a given archive.
*
* @param archive The archive to process, must not be <code>null</code>.
* @throws MojoExecutionException If pre-processing failed.
* @throws MojoExecutionException if pre-processing failed
*/
protected void preProcessArchive(final File archive) throws MojoExecutionException {
// Default implementation does nothing
Expand All @@ -437,14 +437,26 @@ protected void validateParameters() throws MojoExecutionException {
// Default implementation does nothing
}

/**
* Process (sign/verify) a list of archives.
*
* @param archives list of jar files to process
* @throws MojoExecutionException if an error occurs during the processing of archives
*/
protected void processArchives(List<File> archives) throws MojoExecutionException {
for (File file : archives) {
processArchive(file);
}
}

/**
* Processes a given archive.
*
* @param archive The archive to process.
* @throws NullPointerException if {@code archive} is {@code null}.
* @throws MojoExecutionException if processing {@code archive} fails.
* @throws NullPointerException if {@code archive} is {@code null}
* @throws MojoExecutionException if processing {@code archive} fails
*/
private void processArchive(final File archive) throws MojoExecutionException {
protected final void processArchive(final File archive) throws MojoExecutionException {
if (archive == null) {
throw new NullPointerException("archive");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
import java.io.File;
import java.io.IOException;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;

import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
Expand Down Expand Up @@ -115,6 +122,17 @@ public class JarsignerSignMojo extends AbstractJarsignerMojo {
@Parameter(property = "jarsigner.maxRetryDelaySeconds", defaultValue = "0")
private int maxRetryDelaySeconds;

/**
* Maximum number of parallel threads to use when signing jar files. Increases performance when signing multiple jar
* files, especially when network operations are used during signing, for example when using a Time Stamp Authority
* or network based PKCS11 HSM solution for storing code signing keys. Note: the logging from the signing process
* will be interleaved, and harder to read, when using many threads.
*
* @since 3.1.0
*/
@Parameter(property = "jarsigner.threadCount", defaultValue = "1")
private int threadCount;

/** Current WaitStrategy, to allow for sleeping after a signing failure. */
private WaitStrategy waitStrategy = this::defaultWaitStrategy;

Expand Down Expand Up @@ -156,6 +174,11 @@ protected void validateParameters() throws MojoExecutionException {
getLog().warn(getMessage("invalidMaxRetryDelaySeconds", maxRetryDelaySeconds));
maxRetryDelaySeconds = 0;
}

if (threadCount < 1) {
getLog().warn(getMessage("invalidThreadCount", threadCount));
threadCount = 1;
}
}

/**
Expand All @@ -174,12 +197,42 @@ protected JarSignerRequest createRequest(File archive) throws MojoExecutionExcep
return request;
}

/**
* {@inheritDoc} Processing of files may be parallelized for increased performance.
*/
@Override
protected void processArchives(List<File> archives) throws MojoExecutionException {
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
List<Future<Void>> futures = archives.stream()
.map(file -> executor.submit((Callable<Void>) () -> {
processArchive(file);
return null;
}))
.collect(Collectors.toList());
try {
for (Future<Void> future : futures) {
future.get(); // Wait for completion. Result ignored, but may raise any Exception
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new MojoExecutionException("Thread interrupted while waiting for jarsigner to complete", e);
} catch (ExecutionException e) {
if (e.getCause() instanceof MojoExecutionException) {
throw (MojoExecutionException) e.getCause();
}
throw new MojoExecutionException("Error processing archives", e);
} finally {
// Shutdown of thread pool. If an Exception occurred, remaining threads will be aborted "best effort"
executor.shutdownNow();
}
}

/**
* {@inheritDoc}
*
* Will retry signing up to maxTries times if it fails.
*
* @throws MojoExecutionException If all signing attempts fail.
* @throws MojoExecutionException if all signing attempts fail
*/
@Override
protected void executeJarSigner(JarSigner jarSigner, JarSignerRequest request)
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/jarsigner.properties
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ failure = Failed executing ''{0}'' - exitcode {1,number}
archiveNotSigned = Archive ''{0}'' is not signed
invalidMaxTries = Invalid maxTries value. Was ''{0}'' but should be >= 1
invalidMaxRetryDelaySeconds = Invalid maxRetryDelaySeconds value. Was ''{0}'' but should be >= 0
invalidThreadCount = Invalid threadCount value. Was ''{0}'' but should be >= 1
Loading

0 comments on commit 930b7e4

Please sign in to comment.