diff --git a/build.gradle b/build.gradle index 2e1e89f4..d34578e0 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,10 @@ checkstyle { toolVersion = project.checkstyle_version } +tasks.withType(Checkstyle) { + exclude 'job4j/' +} + java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -71,6 +75,7 @@ dependencies { implementation ("io.github.skylot:jadx-java-input:${jadx_version}") { exclude group: 'io.github.skylot', module: 'raung-disasm' } + implementation "org.controlsfx:controlsfx:${controlsfx_version}" runtimeOnly "org.tinylog:tinylog-impl:${tinylog_version}" runtimeOnly "org.tinylog:slf4j-tinylog:${tinylog_version}" diff --git a/gradle.properties b/gradle.properties index 457ad746..89f71bcb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,6 +18,7 @@ jadx_version = 1.4.7 mappingio_version = 0.5.0 javaparser_version = 3.25.6 javafx_version = 21.0.1 +controlsfx_version = 11.2.0 checkstyle_version = 10.12.5 slf4j_version = 2.0.12 tinylog_version = 2.7.0 diff --git a/src/main/java/job4j/Job.java b/src/main/java/job4j/Job.java new file mode 100644 index 00000000..7b4fd3c9 --- /dev/null +++ b/src/main/java/job4j/Job.java @@ -0,0 +1,569 @@ +package job4j; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.DoubleConsumer; + +import job4j.JobSettings.MutableJobSettings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import matcher.Util; + +public abstract class Job implements Runnable { + private static final Logger logger = LoggerFactory.getLogger(Job.class); + private final String id; + private final JobCategory category; + private final MutableJobSettings settings = new MutableJobSettings(); + private volatile T result; + private volatile Throwable error; + private volatile List> subJobs = Collections.synchronizedList(new ArrayList<>()); + private volatile Thread thread; + protected volatile Job parent; + protected volatile double ownProgress = 0; + protected volatile double overallProgress = 0; + protected volatile boolean killed; + protected volatile JobState state = JobState.CREATED; + protected volatile List>> subJobAddedListeners = Collections.synchronizedList(new ArrayList<>()); + protected volatile List progressListeners = Collections.synchronizedList(new ArrayList<>()); + protected volatile List cancelListeners = Collections.synchronizedList(new ArrayList<>()); + protected volatile List, Optional>> completionListeners = Collections.synchronizedList(new ArrayList<>()); + protected volatile List blockingJobCategories = Collections.synchronizedList(new ArrayList<>()); + + public Job(JobCategory category) { + this(category, null); + } + + public Job(JobCategory category, String idAppendix) { + this.category = category; + this.id = category.getId() + (idAppendix == null ? "" : ":" + idAppendix); + + changeDefaultSettings(settings); + } + + + //====================================================================================== + // Overridable methods + //====================================================================================== + + /** + * Override this method to modify the job's default settings. + * Make changes directly on the passed {@link #settings} object. + */ + protected void changeDefaultSettings(MutableJobSettings settings) {} + + /** + * Override this method to register any subjobs known ahead of time. + * Compared to the dynamic {@link #addSubJob} this improves the UX + * by letting the users know which tasks are going to be ran ahead of time + * and giving more accurate progress reports. + */ + protected void registerSubJobs() {}; + + /** + * The main task this job shall execute. Progress is reported on a + * scale from -INF to +1. If this job is only used as an empty shell + * for hosting subjobs, the progressReceiver doesn't have to be invoked, + * then this job's overall progress is automatically calculated + * from the individual subjobs' progresses. + */ + protected abstract T execute(DoubleConsumer progressReceiver); + + + //====================================================================================== + // Listener registration + //====================================================================================== + + /** + * Every time a subjob is registered, the listener gets invoked with the + * newly added job instance. + */ + public void addSubJobAddedListener(Consumer> listener) { + this.subJobAddedListeners.add(listener); + } + + /** + * Every time this job's progress changes, the double consumer gets invoked. + * Progress is a value between -INF and 1, where negative values indicate an uncertain runtime. + */ + public void addProgressListener(DoubleConsumer listener) { + this.progressListeners.add(listener); + } + + /** + * Gets called on job cancellation. The job hasn't completed at this point in time yet, + * it can still run for an indefinite amount of time until it eventually does or does not + * react to the event. + */ + public void addCancelListener(Runnable listener) { + this.cancelListeners.add(listener); + } + + /** + * Gets called once the job is finished. No specific state is guaranteed, + * it has to be checked manually. + * Passes the job's computed result (may be missing or incomplete if canceled/errored early), + * and, if errored, the encountered exception. Errors' stacktraces are printed automatically, + * so it doesn't have to be done manually each time. + */ + public void addCompletionListener(BiConsumer, Optional> listener) { + this.completionListeners.add(listener); + } + + + //====================================================================================== + // User-definable configuration + //====================================================================================== + + /** + * Add IDs of other jobs which must be completed first. + */ + public void addBlockedBy(JobCategory... blockingJobCategories) { + this.blockingJobCategories.addAll(Arrays.asList(blockingJobCategories)); + } + + + //====================================================================================== + // Hierarchy modification + //====================================================================================== + + /** + * Dynamically add subjobs. Please consider overriding {@link #registerSubJobs} + * to register any subjobs known ahead of time! + */ + public void addSubJob(Job subJob, boolean cancelsParentWhenCanceledOrErrored) { + if (hasParentJobInHierarchy(subJob)) { + throw new IllegalArgumentException("Can't add a subjob which is already a parent job!"); + } + + subJob.setParent(this); + subJob.addProgressListener(this::onSubJobProgressChange); + this.subJobs.add(subJob); + + if (cancelsParentWhenCanceledOrErrored) { + subJob.addCancelListener(() -> cancel()); + subJob.addCompletionListener((subJobResult, subJobError) -> { + if (subJobError.isPresent()) { + onError(subJobError.get()); + } + }); + } + + List.copyOf(this.subJobAddedListeners).forEach((listener) -> listener.accept(subJob)); + } + + /** + * Parents are considered effectively final, so don't ever call this method + * while the job is already running. It is only exposed for situations + * where jobs indirectly start other jobs, so that the latter ones can + * be turned into direct children of the caller jobs. + */ + private void setParent(Job parent) { + if (containsSubJob(parent, true)) { + throw new IllegalArgumentException("Can't set an already added subjob as parent job!"); + } + + if (this.state.compareTo(JobState.RUNNING) >= 0) { + throw new UnsupportedOperationException("Can't change job's parent after already having been started"); + } + + this.parent = parent; + } + + + //====================================================================================== + // Lifecycle + //====================================================================================== + + /** + * Queues the job for execution. + * If called on a subjob, executes it directly. + */ + public void run() { + if (this.state.compareTo(JobState.QUEUED) > 0) { + // Already running/finished + return; + } + + this.state = JobState.QUEUED; + + if (this.parent == null) { + // This job is an orphan / top-level job. + // It will be executed on its own thread, + // managed by the JobManager. + JobManager.get().queue(this); + } else { + // This is a subjob. Subjobs get executed + // synchronously directly on the parent thread. + runNow(); + } + } + + /** + * Queues the job for execution, waits for it to get scheduled, + * executes the job and then returns the result and/or error. + * This is basically the synchronous version of registering a + * CompletionListener. + */ + public JobResult runAndAwait() { + if (this.state.compareTo(JobState.QUEUED) > 0) { + // Already running/finished + return new JobResult<>(null, null); + } + + run(); + + while (!this.state.isFinished()) { + try { + Thread.sleep(200); + } catch (InterruptedException e) { + // ignored + } + } + + return new JobResult(result, error); + } + + void runNow() { + if (this.state.compareTo(JobState.QUEUED) > 0) { + // Already running/finished + return; + } + + thread = Thread.currentThread(); + this.state = JobState.RUNNING; + registerSubJobs(); + + try { + this.result = execute(this::onOwnProgressChange); + } catch (Throwable e) { + onError(e); + } + + switch (this.state) { + case RUNNING: + onSuccess(); + break; + case CANCELING: + onCanceled(); + break; + case ERRORED: + break; + default: + throw new IllegalStateException("Job finished running but isn't in a valid state!"); + } + } + + private void onOwnProgressChange(double progress) { + validateProgress(progress); + + if (progress < 1f - Util.floatError && Math.abs(progress - ownProgress) < 0.005) { + // Avoid time consuming computations for + // unnoticeable progress deltas + return; + } + + this.ownProgress = progress; + onProgressChange(); + } + + private void onSubJobProgressChange(double progress) { + validateProgress(progress); + onProgressChange(); + } + + protected void validateProgress(double progress) { + if (progress > 1f + Util.floatError) { + throw new IllegalArgumentException("Progress has to be a value between -INF and 1!"); + } + } + + protected void onProgressChange() { + double progress = 0; + List progresses; + + if (ownProgress < 0 - Util.floatError || ownProgress > 0 + Util.floatError) { + // Own progress has been set. This overrides the automatic + // progress calculation dependent on subjob progress. + progresses = List.of(ownProgress); + } else { + // Don't use own progress if it's never been set. + // This happens if the current job is only used as an + // empty shell for hosting subjobs. + progresses = new ArrayList<>(subJobs.size()); + + for (Job job : List.copyOf(this.subJobs)) { + progresses.add(job.getProgress()); + } + } + + for (double value : progresses) { + if (value < 0) { + progress = -1; + break; + } else { + if (value > 1f + Util.floatError) { + throw new IllegalArgumentException("Progress has to be a value between -INF and 1!"); + } + + progress += value / progresses.size(); + } + } + + this.overallProgress = Math.min(1.0, progress); + List.copyOf(this.progressListeners).forEach(listener -> listener.accept(this.overallProgress)); + } + + public boolean cancel() { + if (this.state != JobState.CANCELING && !this.state.isFinished()) { + onCancel(); + return true; + } + + return false; + } + + protected void onCancel() { + JobState previousState = this.state; + this.state = JobState.CANCELING; + + List.copyOf(this.cancelListeners).forEach(listener -> listener.run()); + List.copyOf(this.subJobs).forEach(job -> job.cancel()); + + if (previousState.compareTo(JobState.RUNNING) < 0) { + onCanceled(); + } + } + + protected void onCanceled() { + this.state = JobState.CANCELED; + onFinish(); + } + + void killRecursive(Throwable error) { + if (this.state.isFinished() || this.killed || this.thread == null) { + return; + } + + this.thread.interrupt(); + this.killed = true; + onError(error); + } + + protected void onError(Throwable error) { + this.state = JobState.ERRORED; + this.error = error; + + if (this.settings.isPrintStackTraceOnError() + && !JobManager.get().isShuttingDown() + && !this.killed) { + logger.error("An exception has been encountered in job '{}':\n{}", + id, Util.getStacktrace(error)); + } + + List.copyOf(this.subJobs).forEach((subJob) -> subJob.cancel()); + + onFinish(); + } + + protected void onSuccess() { + this.state = JobState.SUCCEEDED; + onFinish(); + } + + protected void onFinish() { + onOwnProgressChange(1); + + List.copyOf(this.completionListeners).forEach(listener -> listener.accept(Optional.ofNullable(result), Optional.ofNullable(error))); + } + + + //====================================================================================== + // Getters & Checkers + //====================================================================================== + + Thread getThread() { + return thread; + } + + public String getId() { + return this.id; + } + + public JobCategory getCategory() { + return category; + } + + public Job getParent() { + return this.parent; + } + + public double getProgress() { + return this.overallProgress; + } + + public JobState getState() { + return this.state; + } + + public JobSettings getSettings() { + return settings.getImmutable(); + } + + /** + * {@return an unmodifiable list of subjobs}. + */ + public List> getSubJobs(boolean recursive) { + if (!recursive) { + return Collections.unmodifiableList(this.subJobs); + } + + List> subjobs = List.copyOf(this.subJobs); + List> subjobsRecursive = new ArrayList<>(subjobs); + + for (Job subjob : subjobs) { + subjobsRecursive.addAll(subjob.getSubJobs(true)); + } + + return Collections.unmodifiableList(subjobsRecursive); + } + + public boolean hasSubJob(String id, boolean recursive) { + List> subjobs = List.copyOf(this.subJobs); + boolean hasSubJob = false; + + for (Job subjob : subjobs) { + if (subjob.getId().equals(id)) { + hasSubJob = true; + break; + } + } + + if (!recursive) return hasSubJob; + + for (Job subjob : subjobs) { + if (subjob.hasSubJob(id, recursive)) { + hasSubJob = true; + break; + } + } + + return hasSubJob; + } + + /** + * Checks if this job or any of its subjobs are + * blocked by the passed job category. + */ + public boolean isBlockedBy(JobCategory category) { + boolean blocked = this.blockingJobCategories.contains(category); + + if (blocked) return true; + + blocked = List.copyOf(this.blockingJobCategories).stream() + .filter((blocking) -> category.hasParent(blocking)) + .findAny() + .isPresent(); + + if (blocked) return true; + + return List.copyOf(this.subJobs).stream() + .filter(job -> job.isBlockedBy(category)) + .findAny() + .isPresent(); + } + + public boolean containsSubJob(Job subJob, boolean recursive) { + boolean contains = this.subJobs.contains(subJob); + + if (contains || !recursive) return contains; + + return List.copyOf(this.subJobs).stream() + .filter(nestedSubJob -> nestedSubJob.containsSubJob(subJob, true)) + .findAny() + .isPresent(); + } + + public boolean hasParentJobInHierarchy(Job job) { + if (parent == null) return false; + + return job == parent || parent.hasParentJobInHierarchy(job); + } + + + //====================================================================================== + // Conversions + //====================================================================================== + + public interface JobFuture extends Future { + Job getUnderlyingJob(); + } + + public JobFuture asFuture() { + Job job = this; + + return new JobFuture() { + @Override + public Job getUnderlyingJob() { + return job; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return job.cancel(); + } + + @Override + public boolean isCancelled() { + return job.getState() == JobState.CANCELED; + } + + @Override + public boolean isDone() { + return job.getState().isFinished(); + } + + @Override + public T get() throws InterruptedException, ExecutionException { + job.runAndAwait(); + + if (job.error == null) { + return job.result; + } else if (job.error instanceof InterruptedException) { + throw (InterruptedException) error; + } else if (job.error instanceof ExecutionException) { + throw (ExecutionException) error; + } else { + throw new ExecutionException(error); + } + } + + @Override + public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + job.settings.setTimeout(unit.toSeconds(timeout)); + job.runAndAwait(); + + if (job.error == null) { + return job.result; + } else if (job.error instanceof InterruptedException) { + throw (InterruptedException) error; + } else if (job.error instanceof ExecutionException) { + throw (ExecutionException) error; + } else if (job.error instanceof TimeoutException) { + throw (TimeoutException) error; + } else { + throw new ExecutionException(error); + } + } + }; + } +} diff --git a/src/main/java/job4j/JobCategory.java b/src/main/java/job4j/JobCategory.java new file mode 100644 index 00000000..98e0495e --- /dev/null +++ b/src/main/java/job4j/JobCategory.java @@ -0,0 +1,67 @@ +package job4j; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class JobCategory { + private final String id; + private final List parents = Collections.synchronizedList(new ArrayList<>()); + + public JobCategory(String id) { + this(id, new JobCategory[0]); + } + + public JobCategory(String id, JobCategory... parents) { + this.id = id; + this.parents.addAll(Arrays.asList(parents)); + } + + public String getId() { + return this.id; + } + + public boolean hasParent(JobCategory category) { + synchronized (this.parents) { + return this.parents.stream() + .filter((parent) -> parent.equals(category) || parent.hasParent(category)) + .findAny() + .isPresent(); + } + } + + public boolean hasParent(String categoryId) { + synchronized (this.parents) { + return this.parents.stream() + .filter((parent) -> parent.getId().equals(categoryId) || parent.hasParent(categoryId)) + .findAny() + .isPresent(); + } + } + + public void addParent(JobCategory category) { + this.parents.add(category); + } + + /** + * {@return an unmodifiable view of the parent list}. + */ + public List getParents() { + return Collections.unmodifiableList(this.parents); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof JobCategory)) { + return false; + } + + JobCategory other = (JobCategory) obj; + + boolean sameId = other.getId().equals(this.id); + boolean sameParents = other.getParents().equals(parents); + + return sameId && sameParents; + } +} diff --git a/src/main/java/job4j/JobManager.java b/src/main/java/job4j/JobManager.java new file mode 100644 index 00000000..fa987768 --- /dev/null +++ b/src/main/java/job4j/JobManager.java @@ -0,0 +1,221 @@ +package job4j; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; + +import matcher.Util; + +public class JobManager { + private static final JobManager INSTANCE = new JobManager(); + private static final ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(Math.max(2, Runtime.getRuntime().availableProcessors() / 2)); + + static { + threadPool.setKeepAliveTime(60L, TimeUnit.SECONDS); + threadPool.allowCoreThreadTimeOut(true); + } + + public static synchronized JobManager get() { + return INSTANCE; + } + + private volatile List, JobManagerEvent>> eventListeners = Collections.synchronizedList(new ArrayList<>()); + private volatile List> queuedJobs = Collections.synchronizedList(new LinkedList<>()); + private volatile List> runningJobs = Collections.synchronizedList(new LinkedList<>()); + private volatile boolean shuttingDown; + + public void registerEventListener(BiConsumer, JobManagerEvent> listener) { + this.eventListeners.add(listener); + } + + private void notifyEventListeners(Job job, JobManagerEvent event) { + synchronized (this.eventListeners) { + this.eventListeners.forEach(listener -> listener.accept(job, event)); + } + } + + /** + * Queues the job for execution. + */ + void queue(Job job) { + boolean threadAlreadyExecutingJob = false; + + synchronized (this.runningJobs) { + for (Job runningJob : this.runningJobs) { + if (runningJob.getThread() == Thread.currentThread()) { + // An already running job indirectly started another job. + // Neither one declared the correct hierarchy (they don't know each other), + // nevertheless one job indirectly parents the other one. + // Now we're declaring the correct hierarchy ourselves. + threadAlreadyExecutingJob = true; + runningJob.addSubJob(job, false); + } + } + } + + if (threadAlreadyExecutingJob) { + job.run(); + return; + } + + if (job.getSettings().isCancelPreviousJobsWithSameId()) { + synchronized (queuedJobs) { + for (Job queuedJob : this.queuedJobs) { + if (queuedJob.getCategory() == job.getCategory() + && queuedJob.getId().equals(job.getId())) { + queuedJob.cancel(); + } + } + } + + synchronized (runningJobs) { + for (Job runningJob : this.runningJobs) { + if (runningJob.getCategory() == job.getCategory() + && runningJob.getId().equals(job.getId())) { + runningJob.cancel(); + } + } + } + } + + this.queuedJobs.add(job); + + job.addCompletionListener((result, error) -> onJobFinished(job)); + notifyEventListeners(job, JobManagerEvent.JOB_QUEUED); + tryLaunchNext(); + } + + private void onJobFinished(Job job) { + notifyEventListeners(job, JobManagerEvent.JOB_FINISHED); + this.runningJobs.remove(job); + tryLaunchNext(); + } + + private synchronized void tryLaunchNext() { + for (Job queuedJob : this.queuedJobs) { + boolean blocked = false; + + synchronized (this.runningJobs) { + List> jobsToCheckAgainst = new ArrayList<>(); + + for (Job runningJob : this.runningJobs) { + jobsToCheckAgainst.add(runningJob); + + for (Job runningSubJob : runningJob.getSubJobs(true)) { + jobsToCheckAgainst.add(runningSubJob); + } + } + + for (Job jobToCheckAgainst : jobsToCheckAgainst) { + if (queuedJob.isBlockedBy(jobToCheckAgainst.getCategory())) { + blocked = true; + break; + } + } + } + + if (!blocked) { + this.queuedJobs.remove(queuedJob); + this.runningJobs.add(queuedJob); + notifyEventListeners(queuedJob, JobManagerEvent.JOB_STARTED); + + Thread wrapper = new Thread(() -> { + try { + threadPool.submit(() -> queuedJob.runNow()).get(queuedJob.getSettings().getTimeout(), TimeUnit.SECONDS); + } catch (Throwable e) { + if (e instanceof TimeoutException) { + queuedJob.cancel(); + } else if (!shuttingDown) { + throw new RuntimeException(String.format("An exception has been encountered in job '%s':\n%s", + queuedJob.getId(), Util.getStacktrace(e))); + } + } + }); + wrapper.setName(queuedJob.getId() + " wrapper thread"); + wrapper.start(); + } + } + } + + /** + * {@return an unmodifiable view of the queued jobs list}. + */ + public List> getQueuedJobs() { + return Collections.unmodifiableList(this.queuedJobs); + } + + /** + * {@return an unmodifiable view of the running jobs list}. + */ + public List> getRunningJobs() { + return Collections.unmodifiableList(this.runningJobs); + } + + /** + * @param id the job in question's ID + * @param recursive whether or not all running jobs' subjobs should be checked too + */ + public boolean isJobRunning(String id, boolean recursive) { + List> jobs = List.copyOf(this.runningJobs); + + boolean running = jobs.stream() + .filter((job -> job.getId().equals(id))) + .findAny() + .isPresent(); + + if (!recursive) return running; + + running = jobs.stream() + .filter((job -> job.hasSubJob(id, recursive))) + .findAny() + .isPresent(); + + return running; + } + + public int getMaxJobExecutorThreads() { + return threadPool.getMaximumPoolSize(); + } + + public void setMaxJobExecutorThreads(int maxThreads) { + int oldSize = threadPool.getMaximumPoolSize(); + int newSize = maxThreads; + + if (newSize < oldSize) { + JobManager.threadPool.setCorePoolSize(newSize); + JobManager.threadPool.setMaximumPoolSize(newSize); + } else if (newSize > oldSize) { + JobManager.threadPool.setMaximumPoolSize(newSize); + JobManager.threadPool.setCorePoolSize(newSize); + } + } + + public void shutdown() { + if (shuttingDown) return; + + shuttingDown = true; + this.queuedJobs.clear(); + + synchronized (this.runningJobs) { + this.runningJobs.forEach(job -> job.cancel()); + } + + threadPool.shutdownNow(); + } + + public boolean isShuttingDown() { + return shuttingDown; + } + + public enum JobManagerEvent { + JOB_QUEUED, + JOB_STARTED, + JOB_FINISHED + } +} diff --git a/src/main/java/job4j/JobResult.java b/src/main/java/job4j/JobResult.java new file mode 100644 index 00000000..63be5a51 --- /dev/null +++ b/src/main/java/job4j/JobResult.java @@ -0,0 +1,21 @@ +package job4j; + +import java.util.Optional; + +public class JobResult { + T result; + Throwable error; + + JobResult(T result, Throwable error) { + this.result = result; + this.error = error; + } + + public Optional getResult() { + return Optional.ofNullable(result); + } + + public Optional getError() { + return Optional.ofNullable(error); + } +} diff --git a/src/main/java/job4j/JobSettings.java b/src/main/java/job4j/JobSettings.java new file mode 100644 index 00000000..685f4f8d --- /dev/null +++ b/src/main/java/job4j/JobSettings.java @@ -0,0 +1,100 @@ +package job4j; + +public class JobSettings { + static JobSettings copy(JobSettings original) { + return new JobSettings() {{ + this.invisible = original.invisible; + this.visualPassthrough = original.visualPassthrough; + this.printStackTraceOnError = original.printStackTraceOnError; + this.cancelPreviousJobsWithSameId = original.cancelPreviousJobsWithSameId; + this.timeoutSeconds = original.timeoutSeconds; + }}; + } + + protected boolean invisible; + protected boolean visualPassthrough; + protected boolean printStackTraceOnError = true; + protected boolean cancelPreviousJobsWithSameId; + protected long timeoutSeconds = Long.MAX_VALUE; + + /** + * Whether or not this job and its subjobs should be + * visible to the user. Has no effects on job execution. + */ + public boolean isInvisible() { + return this.invisible; + } + + /** + * Whether or not this job should be visible to the user. + * In contrast to {@link #isInvisible()}, the subjobs + * aren't made invisible too, but instead they appear as + * subjobs of this job's parent (or at the job root, if + * no parent is present). + */ + public boolean isVisualPassthrough() { + return this.visualPassthrough; + } + + public boolean isPrintStackTraceOnError() { + return this.printStackTraceOnError; + } + + /** + * Whether or not already running jobs with the + * same ID should get canceled when this job + * gets submitted. + */ + public boolean isCancelPreviousJobsWithSameId() { + return this.cancelPreviousJobsWithSameId; + } + + /** + * Gets the job's timeout (maximum allowed execution time + * before getting canceled). Defaults to {@link java.lang.Long#MAX_VALUE}. + */ + public long getTimeout() { + return this.timeoutSeconds; + } + + public static class MutableJobSettings extends JobSettings { + private JobSettings immutable; + + void onSettingChange() { + this.immutable = null; + } + + public void makeInvisible() { + this.invisible = true; + onSettingChange(); + } + + public void enableVisualPassthrough() { + this.visualPassthrough = true; + onSettingChange(); + } + + public void dontPrintStacktraceOnError() { + this.printStackTraceOnError = false; + onSettingChange(); + } + + public void cancelPreviousJobsWithSameId() { + this.cancelPreviousJobsWithSameId = true; + onSettingChange(); + } + + public void setTimeout(long seconds) { + this.timeoutSeconds = seconds; + onSettingChange(); + } + + public JobSettings getImmutable() { + if (immutable == null) { + immutable = JobSettings.copy(this); + } + + return immutable; + } + } +} diff --git a/src/main/java/job4j/JobState.java b/src/main/java/job4j/JobState.java new file mode 100644 index 00000000..d8abcc51 --- /dev/null +++ b/src/main/java/job4j/JobState.java @@ -0,0 +1,17 @@ +package job4j; + +public enum JobState { + CREATED, + QUEUED, + RUNNING, + CANCELING, + CANCELED, + ERRORED, + SUCCEEDED; + + public boolean isFinished() { + return this == CANCELED + || this == ERRORED + || this == SUCCEEDED; + } +} diff --git a/src/main/java/job4j/README.md b/src/main/java/job4j/README.md new file mode 100644 index 00000000..b4d4bc03 --- /dev/null +++ b/src/main/java/job4j/README.md @@ -0,0 +1,322 @@ +# Job4j +Job4j is an asynchronous task system for Java. At its core it isn't that much different from other task/worker systems provided by Swing or JavaFX, but in contrast to those, Job4j isn't bound to any UI libraries which may or may not be available at runtime (headless deployment on servers, Graal native etc). + + +## Architecture +Job4j is a pretty small library. In fact, it only provides six classes; though it's important to know how they interact with each other. + +At Job4j's core, there's the `JobManager` class. It is responsible for queueing and running jobs, managing the job executor thread pool, firing job start and job finish events and handling inter-job dependencies. + +Now, the JobManager wouldn't be able to do anything without actual `Job`s. They house the actual task to execute, state management, progress handling etc. It is possible to register event listeners for most `JobState` changes there. You can also block execution of certain jobs when other jobs are running, which is done via `JobCategory`s. Its `JobSettings` are defined on job creation, and turned immutable thereafter. Once a job has finished running, it returns a `JobResult` object. + + +## Usage +`Job` is an abstract class, which can either be extended (for larger, complex tasks) or defined in-line with an anonymous class. Each job has to implement the `execute` method, which houses the main task. Here's an example: +```java +var job = new Job() { + @Override + protected String execute(DoubleConsumer progressReceiver) { + String result = doHeavyComputation(); + return result; + } +} +``` +Several things can be witnessed: +- `new Job`: `String` is the expected return value of the Job. If you don't want to return anything, use `Void`. +- The return value of the `execute` method corresponds to the class passed above. +- `execute` provides a `DoubleConsumer`, which you can pass your current progress. It operates on a scale from 0 to 1, is 0 by default and automatically gets set to 1 when the task finished executing. So you don't have to set those two values manually. + +However, the above example isn't complete yet. When creating a new job, you also need to pass a `JobCategory` instance to the constructor. JobCategories should be declared as `static final` objects in a separate class, so you can reference them from all the classes you're creating jobs in. This is important, since JobCategories are used for: +1. giving your job an ID and +2. managing inter-job dependencies / execution blockers. + +So, a complete example would be: +```java +public class JobCategories { + public static final JobCategory INIT_PROGRAM = new JobCategory("init-program"); + public static final JobCategory DO_HEAVY_COMPUTATION = new JobCategory("do-heavy-computation"); +} + +public class MyClass { + public static void doSomething() { + var job = new Job(JobCategories.DO_HEAVY_COMPUTATION) { + @Override + protected String execute(DoubleConsumer progressReceiver) { + String result = doHeavyComputation(); + return result; + } + } + job.addBlockedBy(JobCategories.INIT_PROGRAM); + } +} + +``` +If a job of the category `INIT_PROGRAM` is running when the above job gets queued, the latter has to wait until the former job is finished before it can run. + +### Overriding default job settings +Changing a job's default settings can be done by overriding the `changeDefaultSettings` method: +```java +var job = new Job(category) { + @Override + protected void changeDefaultSettings(MutableJobSettings settings) { + settings.dontPrintStacktraceOnError(); + } + + @Override + protected Void execute(DoubleConsumer progressReceiver) { + return null; + } +} +``` + +### Adding subjobs +Each job can have an arbitrary amount of subjobs. These can be added at any time during the parent job's lifespan, however it's recommended to register all known subjobs ahead of time in the `registerSubJobs` method. It gets called right before the parent job starts executing, and ensures a great user experience by letting users know which jobs are going to run and how long they're gonna be waiting. +```java +var job = new Job(category) { + @Override + protected void registerSubJobs() { + addSubJob(importantSubJob, true) + addSubJob(unimportantSubJob, false) + } + + @Override + protected Void execute(DoubleConsumer progressReceiver) { + importantSubJob.run(); + unimportantSubJob.run(); + return null; + } +} +``` +A few things to note: +- Subjobs are executed synchronously. +- Once added, subjobs cannot be removed, only canceled. +- Jobs are smart and automatically handle progress correctly. In the example above, when `importantSubJob`'s progress reaches 50%, the overall progress of `job` will be set to 25%. +- When you have subjobs, you usually don't want to pass the parent job's `progressReceiver` any values, as progress is automatically calculated from the subjobs' progress stats anyway. If you _do_ modify the parent jobs' progress, that mechanism gets disabled. +- The second argument passed to `addSubJob` defines whether or not the parent job should be canceled as well, should the passed subjob be canceled or have an error. This is useful for job groups where all other subjobs depend on the calculated result from the preceding subjob. + +### Invisible jobs +If you're adding lots of small subjobs, consider using `JobSettings::makeInvisible`, which hides this job and all its subjobs from potential users' UIs, so they don't get flooded with dozens of UI-cluttering progress bars etc. This option doesn't do anything other than setting a flag, which can then be read by potential GUI implementations. It doesn't change any functionality and the parent's overall progress will still be influenced by them.
+There's a second, similar setting, called `enableVisualPassthrough`. It does basically the same thing, except that only the job you set this on becomes invisible, not however its subjobs. So if you have the following job constellation: +``` +TopLevelJob +└── SubJob + ├── SubSubJob1 + └── SubSubJob2 +``` +and you set `enableVisualPassthrough` on `SubJob`, all of its subjobs will be moving up a level: +``` +TopLevelJob +├── SubSubJob1 +└── SubSubJob2 +``` +Keep in mind that this is purely visual and doesn't influence any behavior, just like `makeInvisible`. It's the responsibility of UI devs to implement correct behavior, Job4j only provides these flags. + +### Running a job +Once you've created the job, you can simply run it via `job.run()`. If the job: +- is a top level job (has no parents), it submits itself to the JobManager queue. +- is a subjob, it gets executed right away (synchronously, on the parent job's thread). + +If you want to execute a job synchronously, you can use the `job.runAndAwait()` method, which behaves more or less like `Future::get`. Jobs don't implement the `Future` interface directly for various reasons, though if you really need to use them as one, you can call `job.asFuture()`. + +If a job, somewhere in the code it executes, indirectly starts another job (without the two knowing of each other), the JobManager will see that they're both on the same thread and therefore adds the newly queued job as a subjob to the already running job. + + +## Job lifecycle +When you create a job, any overridden JobSettings will be applied immediately. The job's ID is also going to be set right away, either taking the supplied `JobCategory`'s ID directly, or, if created via the overloaded constructor, the `JobCategory`'s ID plus a semicolon and the supplied ID-appendix. + +Once you call `job.run()`, its state gets changed to `QUEUED`, and it will continue as described [above](#running-a-job). Once the JobManager decides it's time for the job to start, it will call the internal `Job::runNow` method. This is when the state gets updated to `RUNNING`, all subjobs are registered and the actual task gets executed. Note that subjobs don't get started automatically, they have to be ran individually from the parent job's `execute` method. A parent and its subjobs always run on the same thread. + +Should the `execute` method throw a RuntimeException, the job's subroutines will automatically catch it and set the state to `ERRORED`. If not overridden in the job's settings, the exception gets logged, and, depending on the boolean passed to `Job::addSubJob` earlier, the parent job (if present) will also get canceled. Read [here](#adding-subjobs) for more information.
+If the job gets canceled, it will enter the `CANCELING` state, and remain there until the `execute` method finished. This is why you should regularly check the job's state inside of your `execute` method bodies, so you can stop early if the job is to be canceled. After that, the state gets updated to `CANCELED`.
+Lastly, when a job finishes execution without erroring or getting canceled, it enters the `SUCCEEDED` state. + +Be it `ERRORED`, `CANCELED` or `SUCCEEDED` - all three states indicate that the job has finished running (can be quickly checked via `JobState::isFinished`). Once a job has reached this state, it cannot be restarted. It proceeds to notify all relevant event listeners, and passes them a `JobResult` - consisting of the calculated value (if not errored or canceled too early), and a potential error if an exception occurred. After that, the job is done and gets garbage collected if no references persist. Except when it is a subjob, then it stays alive until the topmost parent job finishes, too. + + +## Thread management +`JobManager` has its own static `ThreadPoolExecutor` instance which is used to execute top level jobs on. By default it uses the formula `Math.max(2, Runtime.getRuntime().availableProcessors() / 2)` to calculate the max amount of threads it will allocate, but this number can always be changed later via `JobManager::setMaxJobExecutorThreads`. + +Here you can see how `Job::run` works internally: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Caller threadWrapper threadJob thread
Caller classJobManagerJobManager anonymous classJob instanceJob execute methodSubjob instanceSubjob execute method
Create job
Configure job
Run job
Check preconditions
Queue job
Create & start wrapper thread
Start job
Cancel job if it reaches timeoutRegister subjobs
Execute task
Run subjob
Execute task
Do something
Update progress
Handle progress update & run event listeners
Handle progress update & run event listeners
Do something
Finish & run event listeners
Do something
Finish & run event listeners
Try to launch next job
+ +`Job::runAndAwait` works similarly, except that it blocks the caller thread with a while loop (and 200ms `Thread::sleep`s) until the job in question finished executing. + + +## Conventions & guidelines +- Put larger jobs into their own classes. Anonymous inner classes get messy really quickly. +- When using the job system in libraries, _always_ use the blocking `Job::runAndAwait`. Library consumers shouldn't have to deal with annoying async stuff. If they want to, they can wrap the method call into a job of their own, and the JobManager will proceed to add your library jobs as children of the library consumer's job. This way, you don't annoy them by default, but developers still have the benefit of seeing what subjobs are being executed when they decide to wrap everything into their own job. +- As already said earlier, try to register subjobs as early as possible, so users can see what's running and better predict how long they'll have to wait. diff --git a/src/main/java/matcher/Matcher.java b/src/main/java/matcher/Matcher.java index 121b604b..0ed17f9b 100644 --- a/src/main/java/matcher/Matcher.java +++ b/src/main/java/matcher/Matcher.java @@ -15,17 +15,18 @@ import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.DoubleConsumer; import java.util.function.Function; -import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; +import job4j.Job; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,6 +39,8 @@ import matcher.classifier.RankResult; import matcher.config.Config; import matcher.config.ProjectConfig; +import matcher.jobs.JobCategories; +import matcher.jobs.MatcherJob; import matcher.type.ClassEnv; import matcher.type.ClassEnvironment; import matcher.type.ClassInstance; @@ -59,19 +62,64 @@ public Matcher(ClassEnvironment env) { this.env = env; } - public void init(ProjectConfig config, DoubleConsumer progressReceiver) { - try { - env.init(config, progressReceiver); + public void init(ProjectConfig config) { + var job = new MatcherJob(JobCategories.LOAD_PROJECT) { + @Override + protected void registerSubJobs() { + Job subJob = new MatcherJob(JobCategories.INIT_ENV) { + @Override + protected Void execute(DoubleConsumer progressReceiver) { + AtomicBoolean shouldCancel = new AtomicBoolean(false); + addCancelListener(() -> shouldCancel.set(true)); + env.init(config, progressReceiver, shouldCancel); + return null; + } + }; + addSubJob(subJob, true); + + subJob = new MatcherJob(JobCategories.MATCH_UNOBFUSCATED) { + @Override + protected Void execute(DoubleConsumer progressReceiver) { + AtomicBoolean shouldCancel = new AtomicBoolean(false); + addCancelListener(() -> shouldCancel.set(true)); + matchUnobfuscated(progressReceiver, shouldCancel); + return null; + } + }; + addSubJob(subJob, false); + } - matchUnobfuscated(); - } catch (Throwable t) { - reset(); - throw t; - } + @Override + protected Void execute(DoubleConsumer progressReceiver) { + for (Job subJob : getSubJobs(false)) { + subJob.run(); + } + + return null; + } + }; + + job.addCompletionListener((result, error) -> { + if (error.isPresent()) { + reset(); + throw new RuntimeException(error.get()); + } + }); + job.runAndAwait(); } - private void matchUnobfuscated() { + private void matchUnobfuscated(DoubleConsumer progressReceiver, AtomicBoolean cancelListener) { + final float classesCount = env.getClassesA().size(); + float classesDone = 0; + for (ClassInstance cls : env.getClassesA()) { + if (cancelListener.get()) { + break; + } + + double progress = classesDone++ / classesCount; + progressReceiver.accept(progress); + if (cls.isNameObfuscated() || !cls.isReal()) continue; ClassInstance match = env.getLocalClsByIdB(cls.getId()); @@ -90,16 +138,12 @@ public ClassEnvironment getEnv() { return env; } - public ClassifierLevel getAutoMatchLevel() { - return autoMatchLevel; - } - public void initFromMatches(List inputDirs, List inputFilesA, List inputFilesB, List cpFiles, List cpFilesA, List cpFilesB, - String nonObfuscatedClassPatternA, String nonObfuscatedClassPatternB, String nonObfuscatedMemberPatternA, String nonObfuscatedMemberPatternB, - DoubleConsumer progressReceiver) throws IOException { + String nonObfuscatedClassPatternA, String nonObfuscatedClassPatternB, + String nonObfuscatedMemberPatternA, String nonObfuscatedMemberPatternB) throws IOException { List pathsA = resolvePaths(inputDirs, inputFilesA); List pathsB = resolvePaths(inputDirs, inputFilesB); List sharedClassPath = resolvePaths(inputDirs, cpFiles); @@ -120,7 +164,7 @@ public void initFromMatches(List inputDirs, Config.saveAsLast(); reset(); - init(config, progressReceiver); + init(config); } public static Path resolvePath(Collection inputDirs, InputFile inputFile) throws IOException { @@ -463,82 +507,6 @@ public void unmatch(MethodVarInstance a) { env.getCache().clear(); } - public void autoMatchAll(DoubleConsumer progressReceiver) { - if (autoMatchClasses(ClassifierLevel.Initial, absClassAutoMatchThreshold, relClassAutoMatchThreshold, progressReceiver)) { - autoMatchClasses(ClassifierLevel.Initial, absClassAutoMatchThreshold, relClassAutoMatchThreshold, progressReceiver); - } - - autoMatchLevel(ClassifierLevel.Intermediate, progressReceiver); - autoMatchLevel(ClassifierLevel.Full, progressReceiver); - autoMatchLevel(ClassifierLevel.Extra, progressReceiver); - - boolean matchedAny; - - do { - matchedAny = autoMatchMethodArgs(ClassifierLevel.Full, absMethodArgAutoMatchThreshold, relMethodArgAutoMatchThreshold, progressReceiver); - matchedAny |= autoMatchMethodVars(ClassifierLevel.Full, absMethodVarAutoMatchThreshold, relMethodVarAutoMatchThreshold, progressReceiver); - } while (matchedAny); - - env.getCache().clear(); - } - - private void autoMatchLevel(ClassifierLevel level, DoubleConsumer progressReceiver) { - boolean matchedAny; - boolean matchedClassesBefore = true; - - do { - matchedAny = autoMatchMethods(level, absMethodAutoMatchThreshold, relMethodAutoMatchThreshold, progressReceiver); - matchedAny |= autoMatchFields(level, absFieldAutoMatchThreshold, relFieldAutoMatchThreshold, progressReceiver); - - if (!matchedAny && !matchedClassesBefore) { - break; - } - - matchedAny |= matchedClassesBefore = autoMatchClasses(level, absClassAutoMatchThreshold, relClassAutoMatchThreshold, progressReceiver); - } while (matchedAny); - } - - public boolean autoMatchClasses(DoubleConsumer progressReceiver) { - return autoMatchClasses(autoMatchLevel, absClassAutoMatchThreshold, relClassAutoMatchThreshold, progressReceiver); - } - - public boolean autoMatchClasses(ClassifierLevel level, double absThreshold, double relThreshold, DoubleConsumer progressReceiver) { - boolean assumeBothOrNoneObfuscated = env.assumeBothOrNoneObfuscated; - Predicate filter = cls -> cls.isReal() && (!assumeBothOrNoneObfuscated || cls.isNameObfuscated()) && !cls.hasMatch() && cls.isMatchable(); - - List classes = env.getClassesA().stream() - .filter(filter) - .collect(Collectors.toList()); - - ClassInstance[] cmpClasses = env.getClassesB().stream() - .filter(filter) - .collect(Collectors.toList()).toArray(new ClassInstance[0]); - - double maxScore = ClassClassifier.getMaxScore(level); - double maxMismatch = maxScore - getRawScore(absThreshold * (1 - relThreshold), maxScore); - Map matches = new ConcurrentHashMap<>(classes.size()); - - runInParallel(classes, cls -> { - List> ranking = ClassClassifier.rank(cls, cmpClasses, level, env, maxMismatch); - - if (checkRank(ranking, absThreshold, relThreshold, maxScore)) { - ClassInstance match = ranking.get(0).getSubject(); - - matches.put(cls, match); - } - }, progressReceiver); - - sanitizeMatches(matches); - - for (Map.Entry entry : matches.entrySet()) { - match(entry.getKey(), entry.getValue()); - } - - LOGGER.info("Auto matched {} classes ({} unmatched, {} total)", matches.size(), (classes.size() - matches.size()), env.getClassesA().size()); - - return !matches.isEmpty(); - } - public static void runInParallel(List workSet, Consumer worker, DoubleConsumer progressReceiver) { if (workSet.isEmpty()) return; @@ -566,47 +534,7 @@ public static void runInParallel(List workSet, Consumer worker, Dou } } - public boolean autoMatchMethods(DoubleConsumer progressReceiver) { - return autoMatchMethods(autoMatchLevel, absMethodAutoMatchThreshold, relMethodAutoMatchThreshold, progressReceiver); - } - - public boolean autoMatchMethods(ClassifierLevel level, double absThreshold, double relThreshold, DoubleConsumer progressReceiver) { - AtomicInteger totalUnmatched = new AtomicInteger(); - Map matches = match(level, absThreshold, relThreshold, - cls -> cls.getMethods(), MethodClassifier::rank, MethodClassifier.getMaxScore(level), - progressReceiver, totalUnmatched); - - for (Map.Entry entry : matches.entrySet()) { - match(entry.getKey(), entry.getValue()); - } - - LOGGER.info("Auto matched {} methods ({} unmatched)", matches.size(), totalUnmatched.get()); - - return !matches.isEmpty(); - } - - public boolean autoMatchFields(DoubleConsumer progressReceiver) { - return autoMatchFields(autoMatchLevel, absFieldAutoMatchThreshold, relFieldAutoMatchThreshold, progressReceiver); - } - - public boolean autoMatchFields(ClassifierLevel level, double absThreshold, double relThreshold, DoubleConsumer progressReceiver) { - AtomicInteger totalUnmatched = new AtomicInteger(); - double maxScore = FieldClassifier.getMaxScore(level); - - Map matches = match(level, absThreshold, relThreshold, - cls -> cls.getFields(), FieldClassifier::rank, maxScore, - progressReceiver, totalUnmatched); - - for (Map.Entry entry : matches.entrySet()) { - match(entry.getKey(), entry.getValue()); - } - - LOGGER.info("Auto matched {} fields ({} unmatched)", matches.size(), totalUnmatched.get()); - - return !matches.isEmpty(); - } - - private > Map match(ClassifierLevel level, double absThreshold, double relThreshold, + public > Map match(ClassifierLevel level, double absThreshold, double relThreshold, Function memberGetter, IRanker ranker, double maxScore, DoubleConsumer progressReceiver, AtomicInteger totalUnmatched) { List classes = env.getClassesA().stream() @@ -649,78 +577,6 @@ private > Map match(ClassifierLevel level, dou return ret; } - public boolean autoMatchMethodArgs(DoubleConsumer progressReceiver) { - return autoMatchMethodArgs(autoMatchLevel, absMethodArgAutoMatchThreshold, relMethodArgAutoMatchThreshold, progressReceiver); - } - - public boolean autoMatchMethodArgs(ClassifierLevel level, double absThreshold, double relThreshold, DoubleConsumer progressReceiver) { - return autoMatchMethodVars(true, MethodInstance::getArgs, level, absThreshold, relThreshold, progressReceiver); - } - - public boolean autoMatchMethodVars(DoubleConsumer progressReceiver) { - return autoMatchMethodVars(autoMatchLevel, absMethodVarAutoMatchThreshold, relMethodVarAutoMatchThreshold, progressReceiver); - } - - public boolean autoMatchMethodVars(ClassifierLevel level, double absThreshold, double relThreshold, DoubleConsumer progressReceiver) { - return autoMatchMethodVars(false, MethodInstance::getVars, level, absThreshold, relThreshold, progressReceiver); - } - - private boolean autoMatchMethodVars(boolean isArg, Function supplier, - ClassifierLevel level, double absThreshold, double relThreshold, DoubleConsumer progressReceiver) { - List methods = env.getClassesA().stream() - .filter(cls -> cls.isReal() && cls.hasMatch() && cls.getMethods().length > 0) - .flatMap(cls -> Stream.of(cls.getMethods())) - .filter(m -> m.hasMatch() && supplier.apply(m).length > 0) - .filter(m -> { - for (MethodVarInstance a : supplier.apply(m)) { - if (!a.hasMatch() && a.isMatchable()) return true; - } - - return false; - }) - .collect(Collectors.toList()); - Map matches; - AtomicInteger totalUnmatched = new AtomicInteger(); - - if (methods.isEmpty()) { - matches = Collections.emptyMap(); - } else { - double maxScore = MethodVarClassifier.getMaxScore(level); - double maxMismatch = maxScore - getRawScore(absThreshold * (1 - relThreshold), maxScore); - matches = new ConcurrentHashMap<>(512); - - runInParallel(methods, m -> { - int unmatched = 0; - - for (MethodVarInstance var : supplier.apply(m)) { - if (var.hasMatch() || !var.isMatchable()) continue; - - List> ranking = MethodVarClassifier.rank(var, supplier.apply(m.getMatch()), level, env, maxMismatch); - - if (checkRank(ranking, absThreshold, relThreshold, maxScore)) { - MethodVarInstance match = ranking.get(0).getSubject(); - - matches.put(var, match); - } else { - unmatched++; - } - } - - if (unmatched > 0) totalUnmatched.addAndGet(unmatched); - }, progressReceiver); - - sanitizeMatches(matches); - } - - for (Map.Entry entry : matches.entrySet()) { - match(entry.getKey(), entry.getValue()); - } - - LOGGER.info("Auto matched {} method {}s ({} unmatched)", matches.size(), (isArg ? "arg" : "var"), totalUnmatched.get()); - - return !matches.isEmpty(); - } - public static boolean checkRank(List> ranking, double absThreshold, double relThreshold, double maxScore) { if (ranking.isEmpty()) return false; @@ -742,7 +598,7 @@ public static double getScore(double rawScore, double maxScore) { return ret * ret; } - private static double getRawScore(double score, double maxScore) { + public static double getRawScore(double score, double maxScore) { return Math.sqrt(score) * maxScore; } @@ -845,19 +701,20 @@ public static class MatchingStatus { public final int matchedFieldCount; } - public static final ExecutorService threadPool = Executors.newWorkStealingPool(); public static final Logger LOGGER = LoggerFactory.getLogger("Matcher"); + public static volatile ForkJoinPool threadPool = (ForkJoinPool) Executors.newWorkStealingPool(4); + public volatile boolean debugMode; private final ClassEnvironment env; - private final ClassifierLevel autoMatchLevel = ClassifierLevel.Extra; - private final double absClassAutoMatchThreshold = 0.85; - private final double relClassAutoMatchThreshold = 0.085; - private final double absMethodAutoMatchThreshold = 0.85; - private final double relMethodAutoMatchThreshold = 0.085; - private final double absFieldAutoMatchThreshold = 0.85; - private final double relFieldAutoMatchThreshold = 0.085; - private final double absMethodArgAutoMatchThreshold = 0.85; - private final double relMethodArgAutoMatchThreshold = 0.085; - private final double absMethodVarAutoMatchThreshold = 0.85; - private final double relMethodVarAutoMatchThreshold = 0.085; + public static final ClassifierLevel defaultAutoMatchLevel = ClassifierLevel.Extra; + public static final double absClassAutoMatchThreshold = 0.85; + public static final double relClassAutoMatchThreshold = 0.085; + public static final double absMethodAutoMatchThreshold = 0.85; + public static final double relMethodAutoMatchThreshold = 0.085; + public static final double absFieldAutoMatchThreshold = 0.85; + public static final double relFieldAutoMatchThreshold = 0.085; + public static final double absMethodArgAutoMatchThreshold = 0.85; + public static final double relMethodArgAutoMatchThreshold = 0.085; + public static final double absMethodVarAutoMatchThreshold = 0.85; + public static final double relMethodVarAutoMatchThreshold = 0.085; } diff --git a/src/main/java/matcher/Util.java b/src/main/java/matcher/Util.java index 33e1fb19..ad1c8827 100644 --- a/src/main/java/matcher/Util.java +++ b/src/main/java/matcher/Util.java @@ -31,6 +31,12 @@ import org.objectweb.asm.tree.MethodInsnNode; public class Util { + public static String getStacktrace(Throwable throwable) { + StringWriter stringWriter = new StringWriter(); + throwable.printStackTrace(new PrintWriter(stringWriter)); + return stringWriter.toString(); + } + public static Set newIdentityHashSet() { return Collections.newSetFromMap(new IdentityHashMap<>()); //new IdentityHashSet<>(); } @@ -373,4 +379,6 @@ public static int compareNatural(String a, String b) { } public static final Object asmNodeSync = new Object(); + /** Max accepted float rounding error. */ + public static final float floatError = 1e-5f; } diff --git a/src/main/java/matcher/gui/BottomPane.java b/src/main/java/matcher/gui/BottomPane.java index e6320d81..20925929 100644 --- a/src/main/java/matcher/gui/BottomPane.java +++ b/src/main/java/matcher/gui/BottomPane.java @@ -6,9 +6,13 @@ import java.util.Map; import java.util.Set; +import org.controlsfx.control.PopOver; +import org.controlsfx.control.PopOver.ArrowLocation; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressBar; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; @@ -38,29 +42,60 @@ private void init() { setId("bottom-pane"); setPadding(new Insets(GuiConstants.padding)); + SelectListener selectListener = new SelectListener(); + srcPane.addListener(selectListener); + dstPane.addListener(selectListener); + + // Left + + HBox left = new HBox(GuiConstants.padding); + getChildren().add(left); + StackPane.setAlignment(left, Pos.CENTER_LEFT); + left.setAlignment(Pos.CENTER_LEFT); + left.setPickOnBounds(false); + + progressBar.setVisible(false); + progressBar.setPrefWidth(200); + + JobProgressView jobProgressView = new JobProgressView(gui); + + PopOver jobPopOver = new PopOver(jobProgressView); + jobPopOver.setAnimated(false); + jobPopOver.setTitle("Active Jobs"); + jobPopOver.setArrowLocation(ArrowLocation.BOTTOM_LEFT); + + progressBar.setOnMouseClicked((e) -> { + jobPopOver.show(progressBar); + }); + + left.getChildren().add(progressBar); + left.getChildren().add(jobLabel); + + // Center + HBox center = new HBox(GuiConstants.padding); getChildren().add(center); StackPane.setAlignment(center, Pos.CENTER); center.setAlignment(Pos.CENTER); + center.setPickOnBounds(false); matchButton.setText("match"); matchButton.setOnAction(event -> match()); - matchButton.setDisable(true); center.getChildren().add(matchButton); matchableButton.setText("unmatchable"); matchableButton.setOnAction(event -> toggleMatchable()); - matchableButton.setDisable(true); center.getChildren().add(matchableButton); matchPerfectMembersButton.setText("match 100% members"); matchPerfectMembersButton.setOnAction(event -> matchPerfectMembers()); - matchPerfectMembersButton.setDisable(true); center.getChildren().add(matchPerfectMembersButton); + // Right + HBox right = new HBox(GuiConstants.padding); getChildren().add(right); StackPane.setAlignment(right, Pos.CENTER_RIGHT); @@ -69,25 +104,20 @@ private void init() { unmatchClassButton.setText("unmatch classes"); unmatchClassButton.setOnAction(event -> unmatchClass()); - unmatchClassButton.setDisable(true); right.getChildren().add(unmatchClassButton); unmatchMemberButton.setText("unmatch members"); unmatchMemberButton.setOnAction(event -> unmatchMember()); - unmatchMemberButton.setDisable(true); right.getChildren().add(unmatchMemberButton); unmatchVarButton.setText("unmatch vars"); unmatchVarButton.setOnAction(event -> unmatchVar()); - unmatchVarButton.setDisable(true); right.getChildren().add(unmatchVarButton); - SelectListener selectListener = new SelectListener(); - srcPane.addListener(selectListener); - dstPane.addListener(selectListener); + updateMatchButtons(); } @Override @@ -97,6 +127,11 @@ public void onMatchChange(Set types) { } } + public void blockMatchButtons(boolean block) { + buttonsBlocked = block; + updateMatchButtons(); + } + private void updateMatchButtons() { ClassInstance clsA = srcPane.getSelectedClass(); ClassInstance clsB = dstPane.getSelectedClass(); @@ -109,7 +144,7 @@ private void updateMatchButtons() { MethodVarInstance varA = srcPane.getSelectedMethodVar(); MethodVarInstance varB = dstPane.getSelectedMethodVar(); - matchButton.setDisable(!canMatchClasses(clsA, clsB) && !canMatchMembers(memberA, memberB) && !canMatchVars(varA, varB)); + matchButton.setDisable(buttonsBlocked || !canMatchClasses(clsA, clsB) && !canMatchMembers(memberA, memberB) && !canMatchVars(varA, varB)); /* * class-null class-matchable class-matched member-null member-matchable member-matched var-null var-matchable var-matched disabled text target * 1 x x x x x x x x 1 unmatchable - @@ -123,17 +158,17 @@ private void updateMatchButtons() { * 0 1 1 0 1 1 0 1 0 0 unmatchable var * 0 1 1 0 1 1 0 1 1 1 unmatchable - */ - matchableButton.setDisable(clsA == null || clsA.isMatchable() && (clsA.hasMatch() || !clsA.hasPotentialMatch()) + matchableButton.setDisable(buttonsBlocked || clsA == null || clsA.isMatchable() && (clsA.hasMatch() || !clsA.hasPotentialMatch()) && (memberA == null || memberA.isMatchable() && (memberA.hasMatch() || !memberA.hasPotentialMatch()) && (varA == null || varA.isMatchable() && (varA.hasMatch() || !varA.hasPotentialMatch())))); matchableButton.setText(clsA != null && (!clsA.isMatchable() || memberA != null && (!memberA.isMatchable() || varA != null && !varA.isMatchable())) ? "matchable" : "unmatchable"); - unmatchClassButton.setDisable(!canUnmatchClass(clsA)); - unmatchMemberButton.setDisable(!canUnmatchMember(memberA)); - unmatchVarButton.setDisable(!canUnmatchVar(varA)); + unmatchClassButton.setDisable(buttonsBlocked || !canUnmatchClass(clsA)); + unmatchMemberButton.setDisable(buttonsBlocked || !canUnmatchMember(memberA)); + unmatchVarButton.setDisable(buttonsBlocked || !canUnmatchVar(varA)); - matchPerfectMembersButton.setDisable(!canMatchPerfectMembers(clsA)); + matchPerfectMembersButton.setDisable(buttonsBlocked || !canMatchPerfectMembers(clsA)); } // match / unmatch actions implementation @@ -375,6 +410,14 @@ public void onMatchListRefresh() { } } + public ProgressBar getProgressBar() { + return progressBar; + } + + public Label getJobLabel() { + return jobLabel; + } + public Button getMatchButton() { return matchButton; } @@ -402,10 +445,14 @@ public Button getUnmatchVarButton() { private final Gui gui; private final MatchPaneSrc srcPane; private final MatchPaneDst dstPane; + private final ProgressBar progressBar = new ProgressBar(); + private final Label jobLabel = new Label(); private final Button matchButton = new Button(); private final Button matchableButton = new Button(); private final Button matchPerfectMembersButton = new Button(); private final Button unmatchClassButton = new Button(); private final Button unmatchMemberButton = new Button(); private final Button unmatchVarButton = new Button(); + + private boolean buttonsBlocked; } diff --git a/src/main/java/matcher/gui/Gui.java b/src/main/java/matcher/gui/Gui.java index f7b5564e..745b7b6a 100644 --- a/src/main/java/matcher/gui/Gui.java +++ b/src/main/java/matcher/gui/Gui.java @@ -8,18 +8,12 @@ import java.util.Collections; import java.util.List; import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.function.Consumer; import java.util.function.DoubleConsumer; import java.util.stream.Collectors; import javafx.application.Application; import javafx.application.Platform; -import javafx.concurrent.Task; -import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Alert; @@ -28,17 +22,19 @@ import javafx.scene.control.Dialog; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; +import javafx.scene.control.Tooltip; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import javafx.scene.layout.RowConstraints; -import javafx.scene.layout.VBox; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.FileChooser.ExtensionFilter; -import javafx.stage.Modality; +import job4j.Job; +import job4j.JobManager; +import job4j.JobManager.JobManagerEvent; +import job4j.JobSettings.MutableJobSettings; import javafx.stage.Stage; -import javafx.stage.StageStyle; import javafx.stage.Window; import net.fabricmc.mappingio.MappingReader; @@ -49,8 +45,10 @@ import matcher.config.ProjectConfig; import matcher.config.Theme; import matcher.gui.IGuiComponent.ViewChangeCause; +import matcher.gui.jobs.GuiJobCategories; import matcher.gui.menu.MainMenuBar; -import matcher.gui.menu.NewProjectPane; +import matcher.gui.panes.NewProjectPane; +import matcher.jobs.MatcherJob; import matcher.mapping.MappingField; import matcher.mapping.Mappings; import matcher.srcprocess.BuiltinDecompiler; @@ -65,6 +63,8 @@ public void start(Stage stage) { env = new ClassEnvironment(); matcher = new Matcher(env); + JobManager.get().registerEventListener((job, event) -> Platform.runLater(() -> onJobManagerEvent(job, event))); + GridPane border = new GridPane(); ColumnConstraints colConstraint = new ColumnConstraints(); @@ -111,7 +111,7 @@ public void start(Stage stage) { @Override public void stop() throws Exception { - threadPool.shutdown(); + JobManager.get().shutdown(); } private void handleStartupArgs(List args) { @@ -229,7 +229,7 @@ private void handleStartupArgs(List args) { newProject(config, inputsA.isEmpty() || inputsB.isEmpty()); } - public CompletableFuture newProject(ProjectConfig config, boolean showConfigDialog) { + public void newProject(ProjectConfig config, boolean showConfigDialog) { ProjectConfig newConfig; if (showConfigDialog) { @@ -246,7 +246,7 @@ public CompletableFuture newProject(ProjectConfig config, boolean showC dialog.setResultConverter(button -> button == ButtonType.OK ? content.createConfig() : null); newConfig = dialog.showAndWait().orElse(null); - if (newConfig == null || !newConfig.isValid()) return CompletableFuture.completedFuture(false); + if (newConfig == null || !newConfig.isValid()) return; } else { newConfig = config; } @@ -257,50 +257,52 @@ public CompletableFuture newProject(ProjectConfig config, boolean showC matcher.reset(); onProjectChange(); - CompletableFuture ret = new CompletableFuture<>(); - - runProgressTask("Initializing files...", - progressReceiver -> { - matcher.init(newConfig, progressReceiver); - ret.complete(true); - }, - () -> { - if (newConfig.getMappingsPathA() != null) { - Path mappingsPath = newConfig.getMappingsPathA(); - - try { - List namespaces = MappingReader.getNamespaces(mappingsPath, null); - Mappings.load(mappingsPath, null, - namespaces.get(0), namespaces.get(1), - MappingField.PLAIN, MappingField.MAPPED, - env.getEnvA(), true); - } catch (IOException e) { - e.printStackTrace(); - } - } - - if (newConfig.getMappingsPathB() != null) { - Path mappingsPath = newConfig.getMappingsPathB(); - - try { - List namespaces = MappingReader.getNamespaces(mappingsPath, null); - Mappings.load(mappingsPath, null, - namespaces.get(0), namespaces.get(1), - MappingField.PLAIN, MappingField.MAPPED, - env.getEnvB(), true); - } catch (IOException e) { - e.printStackTrace(); - } - } - - onProjectChange(); - }, - exc -> { - exc.printStackTrace(); - ret.completeExceptionally(exc); - }); - - return ret; + var job = new MatcherJob(GuiJobCategories.OPEN_NEW_PROJECT) { + @Override + protected void changeDefaultSettings(MutableJobSettings settings) { + settings.enableVisualPassthrough(); + }; + + @Override + protected Void execute(DoubleConsumer progressReceiver) { + menu.updateMenus(false, true); + matcher.init(newConfig); + return null; + } + }; + job.addCompletionListener((result, error) -> Platform.runLater(() -> { + if (newConfig.getMappingsPathA() != null) { + Path mappingsPath = newConfig.getMappingsPathA(); + + try { + List namespaces = MappingReader.getNamespaces(mappingsPath, null); + Mappings.load(mappingsPath, null, + namespaces.get(0), namespaces.get(1), + MappingField.PLAIN, MappingField.MAPPED, + env.getEnvA(), true); + } catch (IOException e) { + e.printStackTrace(); + } + } + + if (newConfig.getMappingsPathB() != null) { + Path mappingsPath = newConfig.getMappingsPathB(); + + try { + List namespaces = MappingReader.getNamespaces(mappingsPath, null); + Mappings.load(mappingsPath, null, + namespaces.get(0), namespaces.get(1), + MappingField.PLAIN, MappingField.MAPPED, + env.getEnvB(), true); + } catch (IOException e) { + e.printStackTrace(); + } + } + + onProjectChange(); + menu.updateMenus(false, false); + })); + job.run(); } public ClassEnvironment getEnv() { @@ -479,70 +481,75 @@ public void onMatchChange(Set types) { } } - public static CompletableFuture runAsyncTask(Callable task) { - Task jfxTask = new Task() { - @Override - protected T call() throws Exception { - return task.call(); - } - }; - - CompletableFuture ret = new CompletableFuture(); - - jfxTask.setOnSucceeded(event -> ret.complete(jfxTask.getValue())); - jfxTask.setOnFailed(event -> ret.completeExceptionally(jfxTask.getException())); - jfxTask.setOnCancelled(event -> ret.cancel(false)); - - threadPool.execute(jfxTask); - - return ret; - } + public void onJobManagerEvent(Job job, JobManagerEvent event) { + switch (event) { + case JOB_STARTED: + activeJobs.add(job); + job.addProgressListener((progress) -> Platform.runLater(() -> onProgressChange(progress))); + break; + case JOB_FINISHED: + activeJobs.remove(job); + break; + } - public void runProgressTask(String labelText, Consumer task) { - runProgressTask(labelText, task, null, null); + updateProgressPane(); } - public void runProgressTask(String labelText, Consumer task, Runnable onSuccess, Consumer onError) { - Stage stage = new Stage(StageStyle.UTILITY); - stage.initOwner(this.scene.getWindow()); - VBox pane = new VBox(GuiConstants.padding); + private void updateProgressPane() { + ProgressBar progressBar = bottomPane.getProgressBar(); + Label jobLabel = bottomPane.getJobLabel(); - stage.setScene(new Scene(pane)); - stage.initModality(Modality.APPLICATION_MODAL); - stage.setOnCloseRequest(event -> event.consume()); - stage.setResizable(false); - stage.setTitle("Operation progress"); + if (activeJobs.size() == 0) { + jobLabel.setText(""); + progressBar.setVisible(false); + progressBar.setProgress(0); + } else { + progressBar.setVisible(true); + + for (Job job : activeJobs) { + if (job.getProgress() <= 0) { + progressBar.setProgress(-1); + break; + } else if (progressBar.getProgress() < 0) { + progressBar.setProgress(job.getProgress() / activeJobs.size()); + } else { + progressBar.setProgress(progressBar.getProgress() + (job.getProgress() / activeJobs.size())); + } + } - pane.setPadding(new Insets(GuiConstants.padding)); + if (activeJobs.size() == 1) { + jobLabel.setText(activeJobs.get(0).getId()); + // progressBar.setProgress(activeJobs.get(0).getProgress()); + } else { + jobLabel.setText(activeJobs.size() + " tasks running"); + StringBuilder tooltipText = new StringBuilder(); - pane.getChildren().add(new Label(labelText)); + for (Job job : activeJobs) { + tooltipText.append(job.getId() + "\n"); + } - ProgressBar progress = new ProgressBar(0); - progress.setPrefWidth(400); - pane.getChildren().add(progress); + jobLabel.setTooltip(new Tooltip(tooltipText.toString())); - stage.show(); + // if (progressBar.getProgress() > 0) { + // progressBar.setProgress(progressBar.getProgress() * (activeJobs.size() - 1) / activeJobs.size()); + // } + } - Task jfxTask = new Task() { - @Override - protected Void call() throws Exception { - task.accept(cProgress -> Platform.runLater(() -> progress.setProgress(cProgress))); + progressBar.setTooltip(new Tooltip(""+progressBar.getProgress())); + } + } - return null; - } - }; + private void onProgressChange(double progress) { + if (activeJobs.size() == 0) return; - jfxTask.setOnSucceeded(event -> { - stage.hide(); - if (onSuccess != null) onSuccess.run(); - }); + ProgressBar progressBar = bottomPane.getProgressBar(); + // bottomPane.getJobLabel().setText(bottomPane.getJobLabel().getText()); - jfxTask.setOnFailed(event -> { - stage.hide(); - if (onError != null) onError.accept(jfxTask.getException()); - }); + // progressBar.setProgress(progressBar.getProgress() + progress / activeJobs.size()); + bottomPane.getJobLabel().setText(String.format("%s (%.0f%%)", + activeJobs.get(0).getId(), progress * 100)); - threadPool.execute(jfxTask); + progressBar.setProgress(progress / activeJobs.size()); } public void showAlert(AlertType type, String title, String headerText, String text) { @@ -641,8 +648,6 @@ public enum SortKey { public static final List> loadListeners = new ArrayList<>(); - private static final ExecutorService threadPool = Executors.newCachedThreadPool(); - private ClassEnvironment env; private Matcher matcher; @@ -654,6 +659,8 @@ public enum SortKey { private MatchPaneDst dstPane; private BottomPane bottomPane; + private List> activeJobs = new ArrayList<>(); + private SortKey sortKey = SortKey.Name; private boolean sortMatchesAlphabetically; private boolean useClassTreeView; diff --git a/src/main/java/matcher/gui/JobProgressView.java b/src/main/java/matcher/gui/JobProgressView.java new file mode 100644 index 00000000..58abb463 --- /dev/null +++ b/src/main/java/matcher/gui/JobProgressView.java @@ -0,0 +1,258 @@ +package matcher.gui; + +import java.util.List; + +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.Control; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.ProgressBar; +import javafx.scene.control.Skin; +import javafx.scene.control.SkinBase; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.VBox; +import job4j.Job; +import job4j.JobManager; +import job4j.JobState; + +public class JobProgressView extends Control { + @SuppressWarnings("incomplete-switch") + public JobProgressView(Gui gui) { + this.gui = gui; + + getStyleClass().add("task-progress-view"); + + JobManager.get().registerEventListener((job, event) -> { + switch (event) { + case JOB_QUEUED: + addJob(job, true); + break; + + case JOB_FINISHED: + Platform.runLater(() -> removeJob(job)); + break; + } + }); + } + + private void addJob(Job job, boolean append) { + if (job.getSettings().isInvisible() && !gui.getMatcher().debugMode) { + return; + } + + job.addSubJobAddedListener((subJob) -> addJob(subJob, false)); + + Platform.runLater(() -> { + if (jobs.contains(job)) return; + + boolean passthrough = job.getSettings().isVisualPassthrough(); + + if (passthrough && !gui.getMatcher().debugMode) { + return; + } + + if (append || jobs.isEmpty()) { + jobs.add(job); + } else { + boolean isParent; + boolean shareParents; + + for (int i = jobs.size() - 1; i >= 0; i--) { + Job currentJob = jobs.get(i); + + isParent = currentJob == job.getParent(); + shareParents = currentJob.hasParentJobInHierarchy(job.getParent()); + + if (isParent || shareParents) { + jobs.add(i+1, job); + break; + } + } + } + }); + } + + private void removeJob(Job job) { + List> subJobs = job.getSubJobs(false); + + synchronized (subJobs) { + for (int i = subJobs.size() - 1; i >= 0; i--) { + removeJob(subJobs.get(i)); + } + } + + jobs.remove(job); + } + + @Override + protected Skin createDefaultSkin() { + return new JobProgressViewSkin(this); + } + + private class JobProgressViewSkin extends SkinBase { + JobProgressViewSkin(JobProgressView progressView) { + super(progressView); + + // list view + ListView> listView = new ListView<>(); + listView.setPrefSize(400, 380); + listView.setPlaceholder(new Label("No tasks running")); + listView.setCellFactory(list -> new TaskCell()); + listView.setFocusTraversable(false); + listView.setPadding(new Insets(GuiConstants.padding, GuiConstants.padding, GuiConstants.padding, GuiConstants.padding)); + + Bindings.bindContent(listView.getItems(), progressView.jobs); + + getChildren().add(listView); + } + + class TaskCell extends ListCell> { + private ProgressBar progressBar; + private Label titleLabel; + private Label progressLabel; + private Button cancelButton; + + private Job job; + private BorderPane borderPane; + private VBox vbox; + + TaskCell() { + titleLabel = new Label(); + titleLabel.getStyleClass().add("task-title"); + + progressLabel = new Label(); + progressLabel.getStyleClass().add("task-message"); + + progressBar = new ProgressBar(); + progressBar.setMaxWidth(Double.MAX_VALUE); + progressBar.setPrefHeight(10); + progressBar.getStyleClass().add("task-progress-bar"); + + cancelButton = new Button("Cancel"); + cancelButton.getStyleClass().add("task-cancel-button"); + cancelButton.setTooltip(new Tooltip("Cancel Task")); + cancelButton.setOnAction(event -> { + if (this.job != null) { + cancelButton.setDisable(true); + this.job.cancel(); + } + }); + + vbox = new VBox(); + vbox.setPadding(new Insets(GuiConstants.padding, 0, 0, GuiConstants.padding)); + vbox.setSpacing(GuiConstants.padding * 0.7f); + vbox.getChildren().add(titleLabel); + vbox.getChildren().add(progressBar); + vbox.getChildren().add(progressLabel); + + BorderPane.setAlignment(cancelButton, Pos.CENTER); + BorderPane.setMargin(cancelButton, new Insets(0, GuiConstants.padding, 0, GuiConstants.padding)); + + borderPane = new BorderPane(); + borderPane.setCenter(vbox); + borderPane.setRight(cancelButton); + setContentDisplay(ContentDisplay.GRAPHIC_ONLY); + } + + private void resetProperties() { + titleLabel.setText(null); + progressLabel.setText(null); + progressBar.setProgress(-1); + progressBar.setStyle(null); + cancelButton.setText("Cancel"); + cancelButton.setDisable(false); + } + + @SuppressWarnings("incomplete-switch") + private void update(Job originatingJob) { + if (originatingJob != this.job) { + return; + } + + if (this.job == null) { + resetProperties(); + return; + } + + if (this.job.getProgress() <= 0) { + progressBar.setProgress(-1); + } else { + progressLabel.setText(String.format("%.0f%%", Math.floor(this.job.getProgress() * 100))); + progressBar.setProgress(this.job.getProgress()); + } + + JobState state = this.job.getState(); + + if (state.isFinished()) { + cancelButton.setDisable(true); + } + + if (state.compareTo(JobState.CANCELING) >= 0) { + cancelButton.setDisable(true); + + String text = state.toString(); + text = text.charAt(0) + text.substring(1).toLowerCase(); + + switch (state) { + case CANCELING: + text += "..."; + break; + case CANCELED: + case ERRORED: + progressBar.setStyle("-fx-accent: darkred"); + break; + } + + cancelButton.setText(text); + } + } + + @Override + protected void updateItem(Job job, boolean empty) { + super.updateItem(job, empty); + this.job = job; + + if (empty || job == null) { + resetProperties(); + getStyleClass().setAll("task-list-cell-empty"); + setGraphic(null); + } else if (job != null) { + job.addCancelListener(() -> Platform.runLater(() -> update(job))); + job.addProgressListener((progress) -> Platform.runLater(() -> update(job))); + job.addCompletionListener((result, error) -> Platform.runLater(() -> update(job))); + + update(job); + getStyleClass().setAll("task-list-cell"); + titleLabel.setText(job.getId()); + + int nestLevel = 0; + Job currentJob = job; + Job currentJobParent; + + while ((currentJobParent = currentJob.getParent()) != null) { + if (!currentJobParent.getSettings().isVisualPassthrough() || gui.getMatcher().debugMode) { + nestLevel++; + } + + currentJob = currentJobParent; + } + + BorderPane.setMargin(vbox, new Insets(0, 0, GuiConstants.padding, GuiConstants.padding * (nestLevel * 5))); + setGraphic(borderPane); + } + } + } + } + + private final Gui gui; + private final ObservableList> jobs = FXCollections.observableArrayList(); +} diff --git a/src/main/java/matcher/gui/MatchPaneDst.java b/src/main/java/matcher/gui/MatchPaneDst.java index c6fc3a8d..adb260d0 100644 --- a/src/main/java/matcher/gui/MatchPaneDst.java +++ b/src/main/java/matcher/gui/MatchPaneDst.java @@ -7,8 +7,8 @@ import java.util.Locale; import java.util.Objects; import java.util.Set; -import java.util.concurrent.Callable; +import javafx.application.Platform; import javafx.scene.control.ListView; import javafx.scene.control.SplitPane; import javafx.scene.control.TextField; @@ -17,17 +17,13 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import job4j.JobSettings.MutableJobSettings; import matcher.Matcher; import matcher.NameType; -import matcher.classifier.ClassClassifier; -import matcher.classifier.ClassifierLevel; -import matcher.classifier.FieldClassifier; -import matcher.classifier.MethodClassifier; -import matcher.classifier.MethodVarClassifier; import matcher.classifier.RankResult; +import matcher.jobs.RankMatchResultsJob; import matcher.type.ClassEnv; -import matcher.type.ClassEnvironment; import matcher.type.ClassInstance; import matcher.type.FieldInstance; import matcher.type.MatchType; @@ -701,41 +697,27 @@ void onSelect(Set matchChangeTypes) { matchList.getItems().clear(); suppressChangeEvents = false; - ClassifierLevel matchLevel = gui.getMatcher().getAutoMatchLevel(); - ClassEnvironment env = gui.getEnv(); - double maxMismatch = Double.POSITIVE_INFINITY; - - Callable>>> ranker; - - if (newSrcSelection == null) { // no class selected + if (newSrcSelection == null) { return; - } else if (newSrcSelection instanceof ClassInstance) { // unmatched class or no member/method var selected - ClassInstance cls = (ClassInstance) newSrcSelection; - ranker = () -> ClassClassifier.rankParallel(cls, cmpClasses.toArray(new ClassInstance[0]), matchLevel, env, maxMismatch); - } else if (newSrcSelection instanceof MethodInstance) { // unmatched method or no method var selected - MethodInstance method = (MethodInstance) newSrcSelection; - ranker = () -> MethodClassifier.rank(method, method.getCls().getMatch().getMethods(), matchLevel, env, maxMismatch); - } else if (newSrcSelection instanceof FieldInstance) { // field - FieldInstance field = (FieldInstance) newSrcSelection; - ranker = () -> FieldClassifier.rank(field, field.getCls().getMatch().getFields(), matchLevel, env, maxMismatch); - } else if (newSrcSelection instanceof MethodVarInstance) { // method arg/var - MethodVarInstance var = (MethodVarInstance) newSrcSelection; - MethodInstance cmpMethod = var.getMethod().getMatch(); - MethodVarInstance[] cmp = var.isArg() ? cmpMethod.getArgs() : cmpMethod.getVars(); - ranker = () -> MethodVarClassifier.rank(var, cmp, matchLevel, env, maxMismatch); - } else { - throw new IllegalStateException(); } - final int cTaskId = ++taskId; - // update matches list - Gui.runAsyncTask(ranker).whenComplete((res, exc) -> { - if (exc != null) { - exc.printStackTrace(); - } else if (taskId == cTaskId) { + final int cJobId = ++jobId; + + var job = new RankMatchResultsJob(gui.getEnv(), Matcher.defaultAutoMatchLevel, newSrcSelection, cmpClasses) { + @Override + protected void changeDefaultSettings(MutableJobSettings settings) { + super.changeDefaultSettings(settings); + settings.cancelPreviousJobsWithSameId(); + } + }; + job.addCompletionListener((results, error) -> Platform.runLater(() -> { + if (jobId == cJobId) { assert rankResults.isEmpty(); - rankResults.addAll(res); + + if (results.isPresent()) { + rankResults.addAll(results.get()); + } updateResults(oldDstSelection, !advancedFilterToggle.isSelected()); oldDstSelection = null; @@ -744,7 +726,8 @@ void onSelect(Set matchChangeTypes) { onMatchChangeApply(matchChangeTypes); } } - }); + })); + job.run(); } private Matchable getMatchableSrcSelection() { @@ -767,7 +750,7 @@ private Matchable getMatchableSrcSelection() { return ret; } - private int taskId; + private int jobId; private Matchable oldSrcSelection; private Matchable oldDstSelection; } diff --git a/src/main/java/matcher/gui/MatchPaneSrc.java b/src/main/java/matcher/gui/MatchPaneSrc.java index 47ee5203..a8c2db45 100644 --- a/src/main/java/matcher/gui/MatchPaneSrc.java +++ b/src/main/java/matcher/gui/MatchPaneSrc.java @@ -266,12 +266,11 @@ private void setCellStyle(Cell cell, Matchable item) { styleClass = CellStyleClass.HIGH_MATCH_SIMILARITY; } } else { - final float epsilon = 1e-5f; // float rounding error float similarity = item.getSimilarity(); - if (similarity < epsilon) { + if (similarity < Util.floatError) { styleClass = CellStyleClass.LOW_MATCH_SIMILARITY; - } else if (similarity > 1 - epsilon) { + } else if (similarity > 1 - Util.floatError) { styleClass = CellStyleClass.HIGH_MATCH_SIMILARITY; } else { cell.setTextFill(null); diff --git a/src/main/java/matcher/gui/jobs/GuiJobCategories.java b/src/main/java/matcher/gui/jobs/GuiJobCategories.java new file mode 100644 index 00000000..00de7bcb --- /dev/null +++ b/src/main/java/matcher/gui/jobs/GuiJobCategories.java @@ -0,0 +1,13 @@ +package matcher.gui.jobs; + +import job4j.JobCategory; + +import matcher.jobs.JobCategories; + +public class GuiJobCategories { + public static final JobCategory OPEN_NEW_PROJECT = new JobCategory("open-new-project"); + + static { + JobCategories.INIT_MATCHER.addParent(OPEN_NEW_PROJECT); + } +} diff --git a/src/main/java/matcher/gui/menu/FileMenu.java b/src/main/java/matcher/gui/menu/FileMenu.java index c64541c3..c0d29a7e 100644 --- a/src/main/java/matcher/gui/menu/FileMenu.java +++ b/src/main/java/matcher/gui/menu/FileMenu.java @@ -10,11 +10,13 @@ import java.util.List; import java.util.Locale; import java.util.Optional; +import java.util.function.DoubleConsumer; import java.util.stream.Stream; import javafx.application.Platform; import javafx.scene.Node; import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.Dialog; import javafx.scene.control.Menu; @@ -31,9 +33,15 @@ import matcher.config.Config; import matcher.gui.Gui; import matcher.gui.Gui.SelectedFile; -import matcher.gui.menu.LoadMappingsPane.MappingsLoadSettings; -import matcher.gui.menu.LoadProjectPane.ProjectLoadSettings; -import matcher.gui.menu.SaveMappingsPane.MappingsSaveSettings; +import matcher.gui.panes.LoadMappingsPane; +import matcher.gui.panes.LoadProjectPane; +import matcher.gui.panes.PreferencesPane; +import matcher.gui.panes.SaveMappingsPane; +import matcher.gui.panes.LoadMappingsPane.MappingsLoadSettings; +import matcher.gui.panes.LoadProjectPane.ProjectLoadSettings; +import matcher.gui.panes.SaveMappingsPane.MappingsSaveSettings; +import matcher.jobs.JobCategories; +import matcher.jobs.MatcherJob; import matcher.mapping.Mappings; import matcher.serdes.MatchesIo; import matcher.type.ClassEnvironment; @@ -45,58 +53,60 @@ public class FileMenu extends Menu { this.gui = gui; - init(); - } - - private void init() { - MenuItem menuItem = new MenuItem("New project"); - getItems().add(menuItem); - menuItem.setOnAction(event -> newProject()); + newProject = new MenuItem("New project"); + getItems().add(newProject); + newProject.setOnAction(event -> newProject()); - menuItem = new MenuItem("Load project"); - getItems().add(menuItem); - menuItem.setOnAction(event -> loadProject()); + loadProject = new MenuItem("Load project"); + getItems().add(loadProject); + loadProject.setOnAction(event -> loadProject()); getItems().add(new SeparatorMenuItem()); - menuItem = new MenuItem("Load mappings"); - getItems().add(menuItem); - menuItem.setOnAction(event -> loadMappings(null)); + loadMappings = new MenuItem("Load mappings"); + getItems().add(loadMappings); + loadMappings.setOnAction(event -> loadMappings(null)); - menuItem = new MenuItem("Load mappings (Enigma dir)"); - getItems().add(menuItem); - menuItem.setOnAction(event -> loadMappings(MappingFormat.ENIGMA_DIR)); + loadMappingsEnigmaDir = new MenuItem("Load mappings (Enigma dir)"); + getItems().add(loadMappingsEnigmaDir); + loadMappingsEnigmaDir.setOnAction(event -> loadMappings(MappingFormat.ENIGMA_DIR)); - menuItem = new MenuItem("Save mappings"); - getItems().add(menuItem); - menuItem.setOnAction(event -> saveMappings(null)); + saveMappings = new MenuItem("Save mappings"); + getItems().add(saveMappings); + saveMappings.setOnAction(event -> saveMappings(null)); - menuItem = new MenuItem("Save mappings (Enigma dir)"); - getItems().add(menuItem); - menuItem.setOnAction(event -> saveMappings(MappingFormat.ENIGMA_DIR)); + saveMappingsEnigmaDir = new MenuItem("Save mappings (Enigma dir)"); + getItems().add(saveMappingsEnigmaDir); + saveMappingsEnigmaDir.setOnAction(event -> saveMappings(MappingFormat.ENIGMA_DIR)); - menuItem = new MenuItem("Clear mappings"); - getItems().add(menuItem); - menuItem.setOnAction(event -> { + clearMappings = new MenuItem("Clear mappings"); + getItems().add(clearMappings); + clearMappings.setOnAction(event -> { Mappings.clear(gui.getMatcher().getEnv()); gui.onMappingChange(); }); getItems().add(new SeparatorMenuItem()); - menuItem = new MenuItem("Load matches"); - getItems().add(menuItem); - menuItem.setOnAction(event -> loadMatches()); + loadMatches = new MenuItem("Load matches"); + getItems().add(loadMatches); + loadMatches.setOnAction(event -> loadMatches()); + + saveMatches = new MenuItem("Save matches"); + getItems().add(saveMatches); + saveMatches.setOnAction(event -> saveMatches()); + + getItems().add(new SeparatorMenuItem()); - menuItem = new MenuItem("Save matches"); - getItems().add(menuItem); - menuItem.setOnAction(event -> saveMatches()); + preferences = new MenuItem("Preferences"); + getItems().add(preferences); + preferences.setOnAction(event -> openPreferences()); getItems().add(new SeparatorMenuItem()); - menuItem = new MenuItem("Exit"); - getItems().add(menuItem); - menuItem.setOnAction(event -> Platform.exit()); + exit = new MenuItem("Exit"); + getItems().add(exit); + exit.setOnAction(event -> Platform.exit()); } private void newProject() { @@ -113,10 +123,15 @@ private void loadProject() { gui.getMatcher().reset(); gui.onProjectChange(); - gui.runProgressTask("Initializing files...", - progressReceiver -> MatchesIo.read(res.path, newConfig.paths, newConfig.verifyFiles, gui.getMatcher(), progressReceiver), - () -> gui.onProjectChange(), - Throwable::printStackTrace); + var job = new MatcherJob(JobCategories.LOAD_PROJECT) { + @Override + protected Void execute(DoubleConsumer progressReceiver) { + MatchesIo.read(res.path, newConfig.paths, newConfig.verifyFiles, gui.getMatcher()); + return null; + } + }; + job.addCompletionListener((result, error) -> Platform.runLater(() -> gui.onProjectChange())); + job.run(); } public ProjectLoadSettings requestProjectLoadSettings() { @@ -339,7 +354,7 @@ private void loadMatches() { SelectedFile res = Gui.requestFile("Select matches file", gui.getScene().getWindow(), getMatchesLoadExtensionFilters(), true); if (res == null) return; - MatchesIo.read(res.path, null, false, gui.getMatcher(), progress -> { }); + MatchesIo.read(res.path, null, false, gui.getMatcher()); gui.onMatchChange(EnumSet.allOf(MatchType.class)); } @@ -374,5 +389,42 @@ private void saveMatches() { } } + private void openPreferences() { + Dialog dialog = new Dialog<>(); + //dialog.initModality(Modality.APPLICATION_MODAL); + dialog.setResizable(true); + dialog.setTitle("Preferences"); + dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + Button okButton = (Button) dialog.getDialogPane().lookupButton(ButtonType.OK); + + PreferencesPane content = new PreferencesPane(okButton); + dialog.getDialogPane().setContent(content); + dialog.showAndWait(); + } + + public void updateMenus(boolean empty, boolean initializing) { + newProject.setDisable(initializing); + loadProject.setDisable(initializing); + loadMappings.setDisable(empty || initializing); + loadMappingsEnigmaDir.setDisable(empty || initializing); + saveMappings.setDisable(empty || initializing); + saveMappingsEnigmaDir.setDisable(empty || initializing); + clearMappings.setDisable(empty || initializing); + loadMatches.setDisable(empty || initializing); + saveMatches.setDisable(empty || initializing); + } + private final Gui gui; + private final MenuItem newProject; + private final MenuItem loadProject; + private final MenuItem loadMappings; + private final MenuItem loadMappingsEnigmaDir; + private final MenuItem saveMappings; + private final MenuItem saveMappingsEnigmaDir; + private final MenuItem clearMappings; + private final MenuItem loadMatches; + private final MenuItem saveMatches; + private final MenuItem preferences; + private final MenuItem exit; } diff --git a/src/main/java/matcher/gui/menu/MainMenuBar.java b/src/main/java/matcher/gui/menu/MainMenuBar.java index ac1939bf..c670580c 100644 --- a/src/main/java/matcher/gui/menu/MainMenuBar.java +++ b/src/main/java/matcher/gui/menu/MainMenuBar.java @@ -19,6 +19,8 @@ private void init() { mappingMenu = addMenu(new MappingMenu(gui)); uidMenu = addMenu(new UidMenu(gui)); viewMenu = addMenu(new ViewMenu(gui)); + + updateMenus(true, false); } private T addMenu(T menu) { @@ -51,6 +53,13 @@ public ViewMenu getViewMenu() { return viewMenu; } + public void updateMenus(boolean empty, boolean initializing) { + fileMenu.updateMenus(empty, initializing); + matchingMenu.setDisable(empty || initializing); + mappingMenu.setDisable(empty || initializing); + uidMenu.setDisable(empty || initializing); + } + private final Gui gui; private FileMenu fileMenu; diff --git a/src/main/java/matcher/gui/menu/MappingMenu.java b/src/main/java/matcher/gui/menu/MappingMenu.java index 2988386d..856f27c7 100644 --- a/src/main/java/matcher/gui/menu/MappingMenu.java +++ b/src/main/java/matcher/gui/menu/MappingMenu.java @@ -1,6 +1,7 @@ package matcher.gui.menu; import java.util.Optional; +import java.util.function.DoubleConsumer; import javafx.scene.control.ButtonType; import javafx.scene.control.Dialog; @@ -8,7 +9,10 @@ import javafx.scene.control.MenuItem; import matcher.gui.Gui; -import matcher.gui.menu.FixRecordNamesPane.NamespaceSettings; +import matcher.gui.panes.FixRecordNamesPane; +import matcher.gui.panes.FixRecordNamesPane.NamespaceSettings; +import matcher.jobs.JobCategories; +import matcher.jobs.MatcherJob; import matcher.mapping.MappingPropagator; public class MappingMenu extends Menu { @@ -23,11 +27,16 @@ public class MappingMenu extends Menu { private void init() { MenuItem menuItem = new MenuItem("Propagate names"); getItems().add(menuItem); - menuItem.setOnAction(event -> gui.runProgressTask( - "Propagating method names/args...", - progressReceiver -> MappingPropagator.propagateNames(gui.getEnv(), progressReceiver), - () -> { }, - Throwable::printStackTrace)); + + var job = new MatcherJob(JobCategories.PROPAGATE_METHOD_NAMES) { + @Override + protected Void execute(DoubleConsumer progressReceiver) { + MappingPropagator.propagateNames(gui.getEnv(), progressReceiver); + return null; + } + }; + + menuItem.setOnAction(event -> job.run()); menuItem = new MenuItem("Fix record member names"); getItems().add(menuItem); diff --git a/src/main/java/matcher/gui/menu/MatchingMenu.java b/src/main/java/matcher/gui/menu/MatchingMenu.java index 48274533..11eeebc6 100644 --- a/src/main/java/matcher/gui/menu/MatchingMenu.java +++ b/src/main/java/matcher/gui/menu/MatchingMenu.java @@ -3,12 +3,19 @@ import java.util.EnumSet; import javafx.scene.control.Alert.AlertType; +import javafx.application.Platform; import javafx.scene.control.Menu; import javafx.scene.control.MenuItem; import javafx.scene.control.SeparatorMenuItem; +import matcher.Matcher; import matcher.Matcher.MatchingStatus; import matcher.gui.Gui; +import matcher.jobs.AutoMatchAllJob; +import matcher.jobs.AutoMatchClassesJob; +import matcher.jobs.AutoMatchFieldsJob; +import matcher.jobs.AutoMatchLocalsJob; +import matcher.jobs.AutoMatchMethodsJob; import matcher.type.MatchType; public class MatchingMenu extends Menu { @@ -55,51 +62,63 @@ private void init() { } public void autoMatchAll() { - gui.runProgressTask( - "Auto matching...", - gui.getMatcher()::autoMatchAll, - () -> gui.onMatchChange(EnumSet.allOf(MatchType.class)), - Throwable::printStackTrace); + var job = new AutoMatchAllJob(gui.getMatcher()); + job.addCompletionListener((result, error) -> { + if (result.isPresent()) { + Platform.runLater(() -> gui.onMatchChange(result.get())); + } + }); + job.run(); } public void autoMatchClasses() { - gui.runProgressTask( - "Auto matching classes...", - gui.getMatcher()::autoMatchClasses, - () -> gui.onMatchChange(EnumSet.allOf(MatchType.class)), - Throwable::printStackTrace); + var job = new AutoMatchClassesJob(gui.getMatcher(), Matcher.defaultAutoMatchLevel); + job.addCompletionListener((matchedAny, error) -> { + if (matchedAny.orElse(false)) { + Platform.runLater(() -> gui.onMatchChange(EnumSet.allOf(MatchType.class))); + } + }); + job.run(); } public void autoMatchMethods() { - gui.runProgressTask( - "Auto matching methods...", - gui.getMatcher()::autoMatchMethods, - () -> gui.onMatchChange(EnumSet.of(MatchType.Method)), - Throwable::printStackTrace); + var job = new AutoMatchMethodsJob(gui.getMatcher(), Matcher.defaultAutoMatchLevel); + job.addCompletionListener((matchedAny, error) -> { + if (matchedAny.orElse(false)) { + Platform.runLater(() -> gui.onMatchChange(EnumSet.of(MatchType.Method))); + } + }); + job.run(); } public void autoMatchFields() { - gui.runProgressTask( - "Auto matching fields...", - gui.getMatcher()::autoMatchFields, - () -> gui.onMatchChange(EnumSet.of(MatchType.Field)), - Throwable::printStackTrace); + var job = new AutoMatchFieldsJob(gui.getMatcher(), Matcher.defaultAutoMatchLevel); + job.addCompletionListener((matchedAny, error) -> { + if (matchedAny.orElse(false)) { + Platform.runLater(() -> gui.onMatchChange(EnumSet.of(MatchType.Field))); + } + }); + job.run(); } public void autoMatchArgs() { - gui.runProgressTask( - "Auto matching method args...", - gui.getMatcher()::autoMatchMethodArgs, - () -> gui.onMatchChange(EnumSet.of(MatchType.MethodVar)), - Throwable::printStackTrace); + var job = new AutoMatchLocalsJob(gui.getMatcher(), Matcher.defaultAutoMatchLevel, true); + job.addCompletionListener((matchedAny, error) -> { + if (matchedAny.orElse(false)) { + Platform.runLater(() -> gui.onMatchChange(EnumSet.of(MatchType.MethodVar))); + } + }); + job.run(); } public void autoMatchVars() { - gui.runProgressTask( - "Auto matching method vars...", - gui.getMatcher()::autoMatchMethodVars, - () -> gui.onMatchChange(EnumSet.of(MatchType.MethodVar)), - Throwable::printStackTrace); + var job = new AutoMatchLocalsJob(gui.getMatcher(), Matcher.defaultAutoMatchLevel, false); + job.addCompletionListener((matchedAny, error) -> { + if (matchedAny.orElse(false)) { + Platform.runLater(() -> gui.onMatchChange(EnumSet.of(MatchType.MethodVar))); + } + }); + job.run(); } public void showMatchingStatus() { diff --git a/src/main/java/matcher/gui/menu/UidMenu.java b/src/main/java/matcher/gui/menu/UidMenu.java index 84df5d9d..27aeb890 100644 --- a/src/main/java/matcher/gui/menu/UidMenu.java +++ b/src/main/java/matcher/gui/menu/UidMenu.java @@ -1,16 +1,10 @@ package matcher.gui.menu; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.net.HttpURLConnection; -import java.net.URL; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; -import java.util.function.DoubleConsumer; +import javafx.application.Platform; import javafx.scene.Node; import javafx.scene.control.ButtonType; import javafx.scene.control.Dialog; @@ -22,15 +16,15 @@ import matcher.config.Config; import matcher.config.UidConfig; import matcher.gui.Gui; -import matcher.type.ClassEnv; +import matcher.gui.panes.UidSetupPane; +import matcher.jobs.ImportMatchesJob; +import matcher.jobs.SubmitMatchesJob; import matcher.type.ClassEnvironment; import matcher.type.ClassInstance; import matcher.type.FieldInstance; -import matcher.type.Matchable; import matcher.type.MatchType; import matcher.type.MemberInstance; import matcher.type.MethodInstance; -import matcher.type.MethodVarInstance; public class UidMenu extends Menu { UidMenu(Gui gui) { @@ -48,21 +42,22 @@ private void init() { getItems().add(new SeparatorMenuItem()); + var importJob = new ImportMatchesJob(gui.getMatcher()); + importJob.addCompletionListener((importedAny, error) -> { + if (importedAny.isPresent()) { + Platform.runLater(() -> gui.onMatchChange(EnumSet.allOf(MatchType.class))); + } + }); + menuItem = new MenuItem("Import matches"); getItems().add(menuItem); - menuItem.setOnAction(event -> gui.runProgressTask( - "Importing matches...", - this::importMatches, - () -> gui.onMatchChange(EnumSet.allOf(MatchType.class)), - Throwable::printStackTrace)); + menuItem.setOnAction(event -> importJob.run()); + + var submitJob = new SubmitMatchesJob(gui.getMatcher()); menuItem = new MenuItem("Submit matches"); getItems().add(menuItem); - menuItem.setOnAction(event -> gui.runProgressTask( - "Submitting matches...", - this::submitMatches, - () -> { }, - Throwable::printStackTrace)); + menuItem.setOnAction(event -> submitJob.run()); getItems().add(new SeparatorMenuItem()); @@ -92,174 +87,6 @@ private void setup() { }); } - private void importMatches(DoubleConsumer progressConsumer) { - UidConfig config = Config.getUidConfig(); - if (!config.isValid()) return; - - try { - HttpURLConnection conn = (HttpURLConnection) new URL("https", - config.getAddress().getHostString(), - config.getAddress().getPort(), - String.format("/%s/matches/%s/%s", config.getProject(), config.getVersionA(), config.getVersionB())).openConnection(); - conn.setRequestProperty("X-Token", config.getToken()); - - progressConsumer.accept(0.5); - - try (DataInputStream is = new DataInputStream(conn.getInputStream())) { - ClassEnvironment env = gui.getEnv(); - Matcher matcher = gui.getMatcher(); - int type; - - while ((type = is.read()) != -1) { - int uid = is.readInt(); - String idA = is.readUTF(); - String idB = is.readUTF(); - - ClassInstance clsA = getCls(env.getEnvA(), idA, type); - ClassInstance clsB = getCls(env.getEnvB(), idB, type); - if (clsA == null || clsB == null) continue; - - switch (type) { - case TYPE_CLASS: - matcher.match(clsA, clsB); - break; - case TYPE_METHOD: - case TYPE_ARG: - case TYPE_VAR: { - MethodInstance methodA = getMethod(clsA, idA, type); - MethodInstance methodB = getMethod(clsB, idB, type); - if (methodA == null || methodB == null) break; - - if (type == TYPE_METHOD) { - matcher.match(methodA, methodB); - } else { - idA = idA.substring(idA.lastIndexOf(')') + 1); - idB = idB.substring(idB.lastIndexOf(')') + 1); - - MethodVarInstance varA = methodA.getVar(idA, type == TYPE_ARG); - MethodVarInstance varB = methodB.getVar(idB, type == TYPE_ARG); - - if (varA != null && varB != null) { - matcher.match(varA, varB); - } - } - - break; - } - case TYPE_FIELD: { - FieldInstance fieldA = getField(clsA, idA); - FieldInstance fieldB = getField(clsB, idB); - if (fieldA == null || fieldB == null) break; - - matcher.match(fieldA, fieldB); - break; - } - } - } - } - - progressConsumer.accept(1); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - - private static ClassInstance getCls(ClassEnv env, String fullId, int type) { - if (type == TYPE_CLASS) { - return env.getLocalClsById(fullId); - } else if (type == TYPE_FIELD) { - int pos = fullId.lastIndexOf('/', fullId.lastIndexOf(";;") - 2); - - return env.getLocalClsById(fullId.substring(0, pos)); - } else { - int pos = fullId.lastIndexOf('/', fullId.lastIndexOf('(') - 1); - - return env.getLocalClsById(fullId.substring(0, pos)); - } - } - - private static MethodInstance getMethod(ClassInstance cls, String fullId, int type) { - int end = type == TYPE_METHOD ? fullId.length() : fullId.lastIndexOf(')') + 1; - - return cls.getMethod(fullId.substring(fullId.lastIndexOf('/', fullId.lastIndexOf('(', end - 1) - 1) + 1, end)); - } - - private static FieldInstance getField(ClassInstance cls, String fullId) { - return cls.getField(fullId.substring(fullId.lastIndexOf('/', fullId.lastIndexOf(";;") - 2) + 1)); - } - - private void submitMatches(DoubleConsumer progressConsumer) { - UidConfig config = Config.getUidConfig(); - if (!config.isValid()) return; - - try { - HttpURLConnection conn = (HttpURLConnection) new URL("https", - config.getAddress().getHostString(), - config.getAddress().getPort(), - String.format("/%s/link/%s/%s", config.getProject(), config.getVersionA(), config.getVersionB())).openConnection(); - conn.setRequestMethod("POST"); - conn.setRequestProperty("X-Token", config.getToken()); - conn.setDoOutput(true); - - List> requested = new ArrayList<>(); - - try (DataOutputStream os = new DataOutputStream(conn.getOutputStream())) { - for (ClassInstance cls : gui.getEnv().getClassesA()) { - if (!cls.hasMatch() || !cls.isInput()) continue; // TODO: skip with known + matched uids - - assert cls.getMatch() != cls; - - requested.add(cls); - os.writeByte(TYPE_CLASS); - os.writeUTF(cls.getId()); - os.writeUTF(cls.getMatch().getId()); - - for (MethodInstance method : cls.getMethods()) { - if (!method.hasMatch() || !method.isReal()) continue; - - String srcMethodId = cls.getId()+"/"+method.getId(); - String dstMethodId = cls.getMatch().getId()+"/"+method.getMatch().getId(); - - requested.add(method); - os.writeByte(TYPE_METHOD); - os.writeUTF(srcMethodId); - os.writeUTF(dstMethodId); - - for (MethodVarInstance arg : method.getArgs()) { - if (!arg.hasMatch()) continue; - - requested.add(arg); - os.writeByte(TYPE_ARG); - os.writeUTF(srcMethodId+arg.getId()); - os.writeUTF(dstMethodId+arg.getMatch().getId()); - } - } - - for (FieldInstance field : cls.getFields()) { - if (!field.hasMatch() || !field.isReal()) continue; - - requested.add(field); - os.writeByte(TYPE_FIELD); - os.writeUTF(cls.getId()+"/"+field.getId()); - os.writeUTF(cls.getMatch().getId()+"/"+field.getMatch().getId()); - } - } - } - - progressConsumer.accept(0.5); - - try (DataInputStream is = new DataInputStream(conn.getInputStream())) { - for (Matchable matchable : requested) { - int uid = is.readInt(); - } - } - - progressConsumer.accept(1); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - private void assignMissing() { ClassEnvironment env = gui.getEnv(); @@ -328,11 +155,5 @@ private void assignMissing() { env.nextFieldUid = nextFieldUid; } - private static final byte TYPE_CLASS = 0; - private static final byte TYPE_METHOD = 1; - private static final byte TYPE_FIELD = 2; - private static final byte TYPE_ARG = 3; - private static final byte TYPE_VAR = 4; - private final Gui gui; } diff --git a/src/main/java/matcher/gui/menu/FixRecordNamesPane.java b/src/main/java/matcher/gui/panes/FixRecordNamesPane.java similarity index 93% rename from src/main/java/matcher/gui/menu/FixRecordNamesPane.java rename to src/main/java/matcher/gui/panes/FixRecordNamesPane.java index 830db996..e3067245 100644 --- a/src/main/java/matcher/gui/menu/FixRecordNamesPane.java +++ b/src/main/java/matcher/gui/panes/FixRecordNamesPane.java @@ -1,4 +1,4 @@ -package matcher.gui.menu; +package matcher.gui.panes; import java.util.ArrayList; import java.util.Arrays; @@ -12,8 +12,8 @@ import matcher.NameType; import matcher.gui.GuiConstants; -class FixRecordNamesPane extends GridPane { - FixRecordNamesPane() { +public class FixRecordNamesPane extends GridPane { + public FixRecordNamesPane() { init(); } diff --git a/src/main/java/matcher/gui/menu/LoadMappingsPane.java b/src/main/java/matcher/gui/panes/LoadMappingsPane.java similarity index 97% rename from src/main/java/matcher/gui/menu/LoadMappingsPane.java rename to src/main/java/matcher/gui/panes/LoadMappingsPane.java index 53c4d279..40043d5e 100644 --- a/src/main/java/matcher/gui/menu/LoadMappingsPane.java +++ b/src/main/java/matcher/gui/panes/LoadMappingsPane.java @@ -1,4 +1,4 @@ -package matcher.gui.menu; +package matcher.gui.panes; import java.util.List; @@ -17,8 +17,8 @@ import matcher.gui.GuiConstants; import matcher.mapping.MappingField; -class LoadMappingsPane extends GridPane { - LoadMappingsPane(List namespaces) { +public class LoadMappingsPane extends GridPane { + public LoadMappingsPane(List namespaces) { init(namespaces); } diff --git a/src/main/java/matcher/gui/menu/LoadProjectPane.java b/src/main/java/matcher/gui/panes/LoadProjectPane.java similarity index 96% rename from src/main/java/matcher/gui/menu/LoadProjectPane.java rename to src/main/java/matcher/gui/panes/LoadProjectPane.java index a45d65d5..19d65ade 100644 --- a/src/main/java/matcher/gui/menu/LoadProjectPane.java +++ b/src/main/java/matcher/gui/panes/LoadProjectPane.java @@ -1,4 +1,4 @@ -package matcher.gui.menu; +package matcher.gui.panes; import java.nio.file.Path; import java.util.ArrayList; @@ -26,7 +26,7 @@ import matcher.gui.GuiUtil; public class LoadProjectPane extends VBox { - LoadProjectPane(List paths, boolean verifyFiles, Window window, Node okButton) { + public LoadProjectPane(List paths, boolean verifyFiles, Window window, Node okButton) { super(GuiConstants.padding); this.paths = FXCollections.observableArrayList(paths); diff --git a/src/main/java/matcher/gui/menu/NewProjectPane.java b/src/main/java/matcher/gui/panes/NewProjectPane.java similarity index 99% rename from src/main/java/matcher/gui/menu/NewProjectPane.java rename to src/main/java/matcher/gui/panes/NewProjectPane.java index 518761d4..f41824c2 100644 --- a/src/main/java/matcher/gui/menu/NewProjectPane.java +++ b/src/main/java/matcher/gui/panes/NewProjectPane.java @@ -1,4 +1,4 @@ -package matcher.gui.menu; +package matcher.gui.panes; import java.io.IOException; import java.nio.file.FileSystems; diff --git a/src/main/java/matcher/gui/panes/PreferencesPane.java b/src/main/java/matcher/gui/panes/PreferencesPane.java new file mode 100644 index 00000000..93116d15 --- /dev/null +++ b/src/main/java/matcher/gui/panes/PreferencesPane.java @@ -0,0 +1,140 @@ +package matcher.gui.panes; + +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; + +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Slider; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import job4j.JobManager; + +import matcher.Matcher; +import matcher.gui.GuiConstants; + +public class PreferencesPane extends VBox { + public PreferencesPane(Button okButton) { + super(GuiConstants.padding); + + this.okButton = okButton; + + init(); + } + + private void init() { + int minSliderValue = 1; + int sliderLabelStep = 2; + int originalMaxSliderValue = Runtime.getRuntime().availableProcessors(); + int alignedMaxSliderValue = ((int) Math.floor((float) originalMaxSliderValue / sliderLabelStep)) * sliderLabelStep; + int normalizedShift = minSliderValue - ((int) Math.floor((float) minSliderValue / sliderLabelStep) * sliderLabelStep); + int distanceRight = normalizedShift; + int distanceLeft = sliderLabelStep - distanceRight; + + if (distanceLeft >= distanceRight) { + alignedMaxSliderValue += distanceRight; + } else { + alignedMaxSliderValue -= distanceLeft; + } + + alignedMaxSliderValue = Math.max(sliderLabelStep, alignedMaxSliderValue); + + Text text = new Text("Main worker threads (only applies after the current workers are finished):"); + text.setWrappingWidth(getWidth()); + getChildren().add(text); + + mainWorkersSlider = newSlider(minSliderValue, alignedMaxSliderValue, sliderLabelStep, Matcher.threadPool.getParallelism()); + mainWorkersSlider.valueProperty().addListener((newValue) -> showWarningIfNecessary()); + getChildren().add(mainWorkersSlider); + + text = new Text("Job executor threads:"); + text.setWrappingWidth(getWidth()); + getChildren().add(text); + + alignedMaxSliderValue = Math.max(2, alignedMaxSliderValue); + jobExecutorsSlider = newSlider(minSliderValue, alignedMaxSliderValue, sliderLabelStep, JobManager.get().getMaxJobExecutorThreads()); + jobExecutorsSlider.valueProperty().addListener((newValue) -> showWarningIfNecessary()); + getChildren().add(jobExecutorsSlider); + + warningText = new Text(); + warningText.setStyle("-fx-fill: firebrick;"); + VBox.setMargin(warningText, new Insets(GuiConstants.padding, 0, GuiConstants.padding, 0)); + getChildren().add(warningText); + showWarningIfNecessary(); + + widthProperty().addListener((observable, oldWidth, newWidth) -> { + for (Node child : getChildren()) { + if (child instanceof Text) { + ((Text) child).setWrappingWidth(newWidth.intValue() - getSpacing() * 5); + } + } + }); + okButton.setOnAction(event -> save()); + setWidth(600); + } + + private Slider newSlider(int min, int max, int labelDistance, int value) { + Slider slider = new Slider(min, max, value); + slider.setShowTickMarks(true); + slider.setShowTickLabels(true); + slider.setMajorTickUnit(labelDistance); + slider.setMinorTickCount(labelDistance - 1); + slider.setBlockIncrement(1); + slider.setSnapToTicks(true); + return slider; + } + + private void showWarningIfNecessary() { + StringBuilder warning = new StringBuilder(); + + int allocatedMegabytes = (int) (Runtime.getRuntime().maxMemory() / 1024 / 1024); + int workerThreadCount = (int) mainWorkersSlider.getValue(); + int minBaseMegabytes = 5200; + int minMegabytesPerThread = 70; + int minTotalRequiredMegabytes = minBaseMegabytes + workerThreadCount * minMegabytesPerThread; + + if (allocatedMegabytes < minBaseMegabytes) { + warning.append("The amount of allocated RAM ("); + warning.append(allocatedMegabytes); + warning.append(" MB) is insufficient! Matcher requires at least "); + warning.append(minBaseMegabytes); + warning.append(" MB to work correctly."); + } else if (allocatedMegabytes < minTotalRequiredMegabytes) { + warning.append("The amount of allocated RAM ("); + warning.append(allocatedMegabytes); + warning.append(" MB) is most likely insufficient for the amount of allocated worker threads! "); + warning.append("Please increase the RAM limit to at least "); + warning.append((int) Math.ceil(minTotalRequiredMegabytes / 100f) * 100); + warning.append(" MB (via the '-Xmx' startup arg)!"); + } + + warningText.setText(warning.toString()); + + if (!warningText.isVisible() && warning.length() > 0) { + warningText.setVisible(true); + requestLayout(); + requestParentLayout(); + } else if (warningText.isVisible() && warning.length() == 0) { + warningText.setVisible(false); + requestLayout(); + requestParentLayout(); + } + } + + private void save() { + int oldThreadPoolSize = Matcher.threadPool.getParallelism(); + int newThreadPoolSize = (int) mainWorkersSlider.getValue(); + + if (newThreadPoolSize != oldThreadPoolSize) { + Matcher.threadPool = (ForkJoinPool) Executors.newWorkStealingPool(newThreadPoolSize); + } + + JobManager.get().setMaxJobExecutorThreads((int) jobExecutorsSlider.getValue()); + } + + private final Button okButton; + private Slider mainWorkersSlider; + private Slider jobExecutorsSlider; + private Text warningText; +} diff --git a/src/main/java/matcher/gui/menu/SaveMappingsPane.java b/src/main/java/matcher/gui/panes/SaveMappingsPane.java similarity index 96% rename from src/main/java/matcher/gui/menu/SaveMappingsPane.java rename to src/main/java/matcher/gui/panes/SaveMappingsPane.java index 82367041..0a765398 100644 --- a/src/main/java/matcher/gui/menu/SaveMappingsPane.java +++ b/src/main/java/matcher/gui/panes/SaveMappingsPane.java @@ -1,4 +1,4 @@ -package matcher.gui.menu; +package matcher.gui.panes; import java.util.List; @@ -16,8 +16,8 @@ import matcher.gui.GuiConstants; import matcher.mapping.MappingsExportVerbosity; -class SaveMappingsPane extends GridPane { - SaveMappingsPane(boolean offerNamespaces) { +public class SaveMappingsPane extends GridPane { + public SaveMappingsPane(boolean offerNamespaces) { init(offerNamespaces); } diff --git a/src/main/java/matcher/gui/menu/UidSetupPane.java b/src/main/java/matcher/gui/panes/UidSetupPane.java similarity index 95% rename from src/main/java/matcher/gui/menu/UidSetupPane.java rename to src/main/java/matcher/gui/panes/UidSetupPane.java index ccfcfa07..a0295d83 100644 --- a/src/main/java/matcher/gui/menu/UidSetupPane.java +++ b/src/main/java/matcher/gui/panes/UidSetupPane.java @@ -1,4 +1,4 @@ -package matcher.gui.menu; +package matcher.gui.panes; import java.net.InetSocketAddress; @@ -13,7 +13,7 @@ import matcher.gui.GuiConstants; public class UidSetupPane extends GridPane { - UidSetupPane(UidConfig config, Window window, Node okButton) { + public UidSetupPane(UidConfig config, Window window, Node okButton) { //this.window = window; this.okButton = okButton; @@ -67,7 +67,7 @@ private void init(UidConfig config) { changeListener.changed(null, null, null); } - UidConfig createConfig() { + public UidConfig createConfig() { int port; try { diff --git a/src/main/java/matcher/gui/tab/SourcecodeTab.java b/src/main/java/matcher/gui/tab/SourcecodeTab.java index 1c782df3..9a510c1d 100644 --- a/src/main/java/matcher/gui/tab/SourcecodeTab.java +++ b/src/main/java/matcher/gui/tab/SourcecodeTab.java @@ -1,12 +1,18 @@ package matcher.gui.tab; -import java.io.PrintWriter; -import java.io.StringWriter; import java.util.Set; +import java.util.function.DoubleConsumer; + +import javafx.application.Platform; +import job4j.JobState; +import job4j.JobSettings.MutableJobSettings; import matcher.NameType; +import matcher.Util; import matcher.gui.Gui; import matcher.gui.ISelectionProvider; +import matcher.jobs.JobCategories; +import matcher.jobs.MatcherJob; import matcher.srcprocess.HtmlUtil; import matcher.srcprocess.SrcDecorator; import matcher.srcprocess.SrcDecorator.SrcParseException; @@ -17,12 +23,12 @@ import matcher.type.MethodInstance; public class SourcecodeTab extends WebViewTab { - public SourcecodeTab(Gui gui, ISelectionProvider selectionProvider, boolean unmatchedTmp) { + public SourcecodeTab(Gui gui, ISelectionProvider selectionProvider, boolean isSource) { super("source", "ui/templates/CodeViewTemplate.htm"); this.gui = gui; this.selectionProvider = selectionProvider; - this.unmatchedTmp = unmatchedTmp; + this.isSource = isSource; update(); } @@ -85,39 +91,46 @@ private void update() { displayText("decompiling..."); - NameType nameType = gui.getNameType().withUnmatchedTmp(unmatchedTmp); - - //Gui.runAsyncTask(() -> gui.getEnv().decompile(selectedClass, true)) - Gui.runAsyncTask(() -> SrcDecorator.decorate(gui.getEnv().decompile(gui.getDecompiler().get(), selectedClass, nameType), selectedClass, nameType)) - .whenComplete((res, exc) -> { - if (cDecompId == decompId) { - if (exc != null) { - exc.printStackTrace(); - - StringWriter sw = new StringWriter(); - exc.printStackTrace(new PrintWriter(sw)); - - if (exc instanceof SrcParseException) { - SrcParseException parseExc = (SrcParseException) exc; - displayText("parse error: "+parseExc.problems+"\ndecompiled source:\n"+parseExc.source); - } else { - displayText("decompile error: "+sw.toString()); - } - } else { - double prevScroll = updateNeeded == 2 ? getScrollTop() : 0; - - displayHtml(res); - - if (updateNeeded == 2 && prevScroll > 0) { - setScrollTop(prevScroll); - } - } - } else if (exc != null) { - exc.printStackTrace(); + NameType nameType = gui.getNameType().withUnmatchedTmp(isSource); + + var decompileJob = new MatcherJob(isSource ? JobCategories.DECOMPILE_SOURCE : JobCategories.DECOMPILE_DEST) { + @Override + protected void changeDefaultSettings(MutableJobSettings settings) { + settings.dontPrintStacktraceOnError(); + settings.cancelPreviousJobsWithSameId(); + } + + @Override + protected String execute(DoubleConsumer progressReceiver) { + return SrcDecorator.decorate(gui.getEnv().decompile(gui.getDecompiler().get(), selectedClass, nameType), selectedClass, nameType); + } + }; + decompileJob.addCompletionListener((code, error) -> Platform.runLater(() -> { + if (cDecompId == decompId) { + if (code.isEmpty() && decompileJob.getState() == JobState.CANCELED) { + // The job got canceled before any code was generated. Ignore any errors. + return; + } + + if (error.isPresent()) { + if (error.get() instanceof SrcParseException) { + SrcParseException parseExc = (SrcParseException) error.get(); + displayText("parse error: " + parseExc.problems + "\ndecompiled source:\n" + parseExc.source); + } else { + displayText("decompile error: " + Util.getStacktrace(error.get())); } + } else if (code.isPresent()) { + double prevScroll = updateNeeded == 2 ? getScrollTop() : 0; + + displayHtml(code.get()); - updateNeeded = 0; - }); + if (updateNeeded == 2 && prevScroll > 0) { + setScrollTop(prevScroll); + } + } + } + })); + decompileJob.run(); } @Override @@ -140,7 +153,7 @@ public void onFieldSelect(FieldInstance field) { private final Gui gui; private final ISelectionProvider selectionProvider; - private final boolean unmatchedTmp; + private final boolean isSource; private int decompId; private int updateNeeded; diff --git a/src/main/java/matcher/jobs/AutoMatchAllJob.java b/src/main/java/matcher/jobs/AutoMatchAllJob.java new file mode 100644 index 00000000..b1320f70 --- /dev/null +++ b/src/main/java/matcher/jobs/AutoMatchAllJob.java @@ -0,0 +1,247 @@ +package matcher.jobs; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.function.DoubleConsumer; + +import job4j.Job; +import job4j.JobState; +import job4j.JobSettings.MutableJobSettings; + +import matcher.Matcher; +import matcher.classifier.ClassifierLevel; +import matcher.type.MatchType; + +public class AutoMatchAllJob extends MatcherJob> { + public AutoMatchAllJob(Matcher matcher) { + super(JobCategories.AUTOMATCH_ALL); + + this.matcher = matcher; + } + + @Override + protected Set execute(DoubleConsumer progressReceiver) { + for (Job job : getSubJobs(false)) { + if (state == JobState.CANCELING) { + break; + } + + job.run(); + } + + matcher.getEnv().getCache().clear(); + + Set matchedTypes = new HashSet<>(); + + if (matchedAnyClasses) { + matchedTypes.add(MatchType.Class); + } + + if (matchedAnyMembers) { + matchedTypes.add(MatchType.Method); + matchedTypes.add(MatchType.Field); + } + + if (matchedAnyLocals) { + matchedTypes.add(MatchType.MethodVar); + } + + return matchedTypes; + } + + @Override + protected void registerSubJobs() { + Job job; + + // Automatch classes, pass 1 + job = new AutoMatchClassesJob(matcher, ClassifierLevel.Initial); + job.addCompletionListener(this::onMatchedClasses); + addSubJob(job, true); + + // Automatch classes, pass 2 + job = new AutoMatchClassesJob(matcher, ClassifierLevel.Initial) { + @Override + protected Boolean execute(DoubleConsumer progressReceiver) { + if (!matchedAnyClasses) { + // No matches were found in the last pass, + // so we aren't going to find any either. + return false; + } + + return super.execute(progressReceiver); + } + }; + job.addCompletionListener(this::onMatchedClasses); + addSubJob(job, false); + + // Automatch all: intermediate + job = new MatcherJob<>(JobCategories.AUTOMATCH_ALL_INTERMEDIATE) { + @Override + protected Boolean execute(DoubleConsumer progressReceiver) { + return autoMatchMembers(ClassifierLevel.Intermediate, this); + } + }; + addSubJob(job, false); + + // Automatch all: full + job = new MatcherJob<>(JobCategories.AUTOMATCH_ALL_FULL) { + @Override + protected Boolean execute(DoubleConsumer progressReceiver) { + return autoMatchMembers(ClassifierLevel.Full, this); + }; + }; + addSubJob(job, false); + + // Automatch all: extra + job = new MatcherJob<>(JobCategories.AUTOMATCH_ALL_EXTRA) { + @Override + protected Boolean execute(DoubleConsumer progressReceiver) { + return autoMatchMembers(ClassifierLevel.Extra, this); + }; + }; + addSubJob(job, false); + + // Automatch locals + job = new MatcherJob(JobCategories.AUTOMATCH_ALL_LOCALS) { + @Override + protected Boolean execute(DoubleConsumer progressReceiver) { + return autoMatchLocals(this); + }; + }; + addSubJob(job, false); + } + + private boolean autoMatchMembers(ClassifierLevel level, Job parentJob) { + if (parentJob.getState() == JobState.CANCELING) { + return false; + } + + boolean matchedAny = false; + boolean matchedAnyOverall = false; + boolean matchedAnyClassesBefore = true; + + do { + matchedAny = false; + + // Register method matching subjob + var methodJob = new AutoMatchMethodsJob(matcher, level) { + @Override + protected void changeDefaultSettings(MutableJobSettings settings) { + super.changeDefaultSettings(settings); + settings.makeInvisible(); + } + }; + methodJob.addCompletionListener(this::onMatchedMembers); + parentJob.addSubJob(methodJob, false); + + // Register field matching subjob + var fieldJob = new AutoMatchFieldsJob(matcher, level) { + @Override + protected void changeDefaultSettings(MutableJobSettings settings) { + super.changeDefaultSettings(settings); + settings.makeInvisible(); + } + }; + fieldJob.addCompletionListener(this::onMatchedMembers); + parentJob.addSubJob(fieldJob, false); + + // Register class matching subjob + var classesJob = new AutoMatchClassesJob(matcher, level) { + @Override + protected void changeDefaultSettings(MutableJobSettings settings) { + super.changeDefaultSettings(settings); + settings.makeInvisible(); + } + }; + classesJob.addCompletionListener(this::onMatchedClasses); + parentJob.addSubJob(classesJob, false); + + // Run subjobs + matchedAny |= methodJob.runAndAwait().getResult().orElse(false); + matchedAnyOverall |= matchedAny; + + if (parentJob.getState() == JobState.CANCELING) { + break; + } + + matchedAny |= fieldJob.runAndAwait().getResult().orElse(false); + matchedAnyOverall |= matchedAny; + + if (parentJob.getState() == JobState.CANCELING + || (!matchedAny && !matchedAnyClassesBefore)) { + classesJob.cancel(); + break; + } + + matchedAnyClassesBefore = classesJob.runAndAwait().getResult().orElse(false); + } while (matchedAny && parentJob.getState() != JobState.CANCELING); + + return matchedAnyOverall; + } + + private boolean autoMatchLocals(Job parentJob) { + if (parentJob.getState() == JobState.CANCELING) { + return false; + } + + boolean matchedAnyOverall = false; + boolean matchedAny; + + do { + matchedAny = false; + + // Register arg matching subjob + var argJob = new AutoMatchLocalsJob(matcher, ClassifierLevel.Full, true) { + @Override + protected void changeDefaultSettings(MutableJobSettings settings) { + super.changeDefaultSettings(settings); + settings.makeInvisible(); + } + }; + argJob.addCompletionListener(this::onMatchedLocals); + parentJob.addSubJob(argJob, false); + + // Register var matching subjob + var varJob = new AutoMatchLocalsJob(matcher, ClassifierLevel.Full, false) { + @Override + protected void changeDefaultSettings(MutableJobSettings settings) { + super.changeDefaultSettings(settings); + settings.makeInvisible(); + } + }; + varJob.addCompletionListener(this::onMatchedLocals); + parentJob.addSubJob(varJob, false); + + // Run subjobs + matchedAny |= argJob.runAndAwait().getResult().orElse(false); + matchedAnyOverall |= matchedAny; + + if (parentJob.getState() == JobState.CANCELING) { + break; + } + + matchedAny |= varJob.runAndAwait().getResult().orElse(false); + matchedAnyOverall |= matchedAny; + } while (matchedAny && parentJob.getState() != JobState.CANCELING); + + return matchedAnyOverall; + } + + private void onMatchedClasses(Optional matchedAny, Optional error) { + matchedAnyClasses |= matchedAny.orElse(false); + } + + private void onMatchedMembers(Optional matchedAny, Optional error) { + matchedAnyMembers |= matchedAny.orElse(false); + } + + private void onMatchedLocals(Optional matchedAny, Optional error) { + matchedAnyLocals |= matchedAny.orElse(false); + } + + private final Matcher matcher; + private boolean matchedAnyClasses; + private boolean matchedAnyMembers; + private boolean matchedAnyLocals; +} diff --git a/src/main/java/matcher/jobs/AutoMatchClassesJob.java b/src/main/java/matcher/jobs/AutoMatchClassesJob.java new file mode 100644 index 00000000..4d42f07d --- /dev/null +++ b/src/main/java/matcher/jobs/AutoMatchClassesJob.java @@ -0,0 +1,72 @@ +package matcher.jobs; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.DoubleConsumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import job4j.JobState; + +import matcher.Matcher; +import matcher.classifier.ClassClassifier; +import matcher.classifier.ClassifierLevel; +import matcher.classifier.RankResult; +import matcher.type.ClassEnvironment; +import matcher.type.ClassInstance; + +public class AutoMatchClassesJob extends MatcherJob { + public AutoMatchClassesJob(Matcher matcher, ClassifierLevel level) { + super(JobCategories.AUTOMATCH_CLASSES); + + this.matcher = matcher; + this.level = level; + } + + @Override + protected Boolean execute(DoubleConsumer progressReceiver) { + ClassEnvironment env = matcher.getEnv(); + boolean assumeBothOrNoneObfuscated = env.assumeBothOrNoneObfuscated; + Predicate filter = cls -> cls.isReal() && (!assumeBothOrNoneObfuscated || cls.isNameObfuscated()) && !cls.hasMatch() && cls.isMatchable(); + + List classes = env.getClassesA().stream() + .filter(filter) + .collect(Collectors.toList()); + + ClassInstance[] cmpClasses = env.getClassesB().stream() + .filter(filter) + .collect(Collectors.toList()).toArray(new ClassInstance[0]); + + double maxScore = ClassClassifier.getMaxScore(level); + double maxMismatch = maxScore - Matcher.getRawScore(Matcher.absClassAutoMatchThreshold * (1 - Matcher.relClassAutoMatchThreshold), maxScore); + Map matches = new ConcurrentHashMap<>(classes.size()); + + Matcher.runInParallel(classes, cls -> { + if (state == JobState.CANCELING) { + return; + } + + List> ranking = ClassClassifier.rank(cls, cmpClasses, level, env, maxMismatch); + + if (Matcher.checkRank(ranking, Matcher.absClassAutoMatchThreshold, Matcher.relClassAutoMatchThreshold, maxScore)) { + ClassInstance match = ranking.get(0).getSubject(); + + matches.put(cls, match); + } + }, progressReceiver); + + Matcher.sanitizeMatches(matches); + + for (Map.Entry entry : matches.entrySet()) { + matcher.match(entry.getKey(), entry.getValue()); + } + + Matcher.LOGGER.info("Auto matched {} classes ({} unmatched, {} total)", matches.size(), (classes.size() - matches.size()), env.getClassesA().size()); + + return !matches.isEmpty(); + } + + private final Matcher matcher; + private final ClassifierLevel level; +} diff --git a/src/main/java/matcher/jobs/AutoMatchFieldsJob.java b/src/main/java/matcher/jobs/AutoMatchFieldsJob.java new file mode 100644 index 00000000..4d450755 --- /dev/null +++ b/src/main/java/matcher/jobs/AutoMatchFieldsJob.java @@ -0,0 +1,41 @@ +package matcher.jobs; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.DoubleConsumer; + +import matcher.Matcher; +import matcher.classifier.ClassifierLevel; +import matcher.classifier.FieldClassifier; +import matcher.type.FieldInstance; + +public class AutoMatchFieldsJob extends MatcherJob { + public AutoMatchFieldsJob(Matcher matcher, ClassifierLevel level) { + super(JobCategories.AUTOMATCH_FIELDS); + + this.matcher = matcher; + this.level = level; + } + + @Override + protected Boolean execute(DoubleConsumer progressReceiver) { + AtomicInteger totalUnmatched = new AtomicInteger(); + double maxScore = FieldClassifier.getMaxScore(level); + + Map matches = matcher.match(level, + Matcher.absFieldAutoMatchThreshold, Matcher.relFieldAutoMatchThreshold, + cls -> cls.getFields(), FieldClassifier::rank, maxScore, + progressReceiver, totalUnmatched); + + for (Map.Entry entry : matches.entrySet()) { + matcher.match(entry.getKey(), entry.getValue()); + } + + Matcher.LOGGER.info("Auto matched {} fields ({} unmatched)", matches.size(), totalUnmatched.get()); + + return !matches.isEmpty(); + } + + private final Matcher matcher; + private final ClassifierLevel level; +} diff --git a/src/main/java/matcher/jobs/AutoMatchLocalsJob.java b/src/main/java/matcher/jobs/AutoMatchLocalsJob.java new file mode 100644 index 00000000..5fc8a80c --- /dev/null +++ b/src/main/java/matcher/jobs/AutoMatchLocalsJob.java @@ -0,0 +1,111 @@ +package matcher.jobs; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.DoubleConsumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import job4j.JobState; + +import matcher.Matcher; +import matcher.classifier.ClassifierLevel; +import matcher.classifier.MethodVarClassifier; +import matcher.classifier.RankResult; +import matcher.type.MethodInstance; +import matcher.type.MethodVarInstance; + +public class AutoMatchLocalsJob extends MatcherJob { + public AutoMatchLocalsJob(Matcher matcher, ClassifierLevel level, boolean args) { + super(JobCategories.AUTOMATCH_LOCALS); + + this.matcher = matcher; + this.level = level; + this.args = args; + } + + @Override + protected Boolean execute(DoubleConsumer progressReceiver) { + Function supplier; + double absThreshold, relThreshold; + + if (args) { + supplier = MethodInstance::getArgs; + absThreshold = Matcher.absMethodArgAutoMatchThreshold; + relThreshold = Matcher.relMethodArgAutoMatchThreshold; + } else { + supplier = MethodInstance::getVars; + absThreshold = Matcher.absMethodVarAutoMatchThreshold; + relThreshold = Matcher.relMethodVarAutoMatchThreshold; + } + + List methods = matcher.getEnv().getClassesA().stream() + .filter(cls -> cls.isReal() && cls.hasMatch() && cls.getMethods().length > 0) + .flatMap(cls -> Stream.of(cls.getMethods())) + .filter(m -> m.hasMatch() && supplier.apply(m).length > 0) + .filter(m -> { + for (MethodVarInstance a : supplier.apply(m)) { + if (!a.hasMatch() && a.isMatchable()) return true; + } + + return false; + }) + .collect(Collectors.toList()); + Map matches; + AtomicInteger totalUnmatched = new AtomicInteger(); + + if (methods.isEmpty()) { + matches = Collections.emptyMap(); + } else { + double maxScore = MethodVarClassifier.getMaxScore(level); + double maxMismatch = maxScore - Matcher.getRawScore(absThreshold * (1 - relThreshold), maxScore); + matches = new ConcurrentHashMap<>(512); + + Matcher.runInParallel(methods, m -> { + int unmatched = 0; + + for (MethodVarInstance var : supplier.apply(m)) { + if (state == JobState.CANCELING) { + break; + } + + if (var.hasMatch() || !var.isMatchable()) continue; + + List> ranking = MethodVarClassifier.rank(var, supplier.apply(m.getMatch()), level, matcher.getEnv(), maxMismatch); + + if (Matcher.checkRank(ranking, absThreshold, relThreshold, maxScore)) { + MethodVarInstance match = ranking.get(0).getSubject(); + + matches.put(var, match); + } else { + unmatched++; + } + } + + if (unmatched > 0) totalUnmatched.addAndGet(unmatched); + }, progressReceiver); + + Matcher.sanitizeMatches(matches); + } + + for (Map.Entry entry : matches.entrySet()) { + if (state == JobState.CANCELING) { + break; + } + + matcher.match(entry.getKey(), entry.getValue()); + } + + Matcher.LOGGER.info("Auto matched {} method {}s ({} unmatched)", matches.size(), (args ? "arg" : "var"), totalUnmatched.get()); + + return !matches.isEmpty(); + } + + private final Matcher matcher; + private final ClassifierLevel level; + private final boolean args; +} diff --git a/src/main/java/matcher/jobs/AutoMatchMethodsJob.java b/src/main/java/matcher/jobs/AutoMatchMethodsJob.java new file mode 100644 index 00000000..61ad1dc6 --- /dev/null +++ b/src/main/java/matcher/jobs/AutoMatchMethodsJob.java @@ -0,0 +1,44 @@ +package matcher.jobs; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.DoubleConsumer; + +import job4j.JobState; + +import matcher.Matcher; +import matcher.classifier.ClassifierLevel; +import matcher.classifier.MethodClassifier; +import matcher.type.MethodInstance; + +public class AutoMatchMethodsJob extends MatcherJob { + public AutoMatchMethodsJob(Matcher matcher, ClassifierLevel level) { + super(JobCategories.AUTOMATCH_METHODS); + + this.matcher = matcher; + this.level = level; + } + + @Override + protected Boolean execute(DoubleConsumer progressReceiver) { + AtomicInteger totalUnmatched = new AtomicInteger(); + Map matches = matcher.match(level, Matcher.absMethodAutoMatchThreshold, Matcher.relMethodAutoMatchThreshold, + cls -> cls.getMethods(), MethodClassifier::rank, MethodClassifier.getMaxScore(level), + progressReceiver, totalUnmatched); + + for (Map.Entry entry : matches.entrySet()) { + if (state == JobState.CANCELING) { + break; + } + + matcher.match(entry.getKey(), entry.getValue()); + } + + Matcher.LOGGER.info("Auto matched {} methods ({} unmatched)", matches.size(), totalUnmatched.get()); + + return !matches.isEmpty(); + } + + private final Matcher matcher; + private final ClassifierLevel level; +} diff --git a/src/main/java/matcher/jobs/ImportMatchesJob.java b/src/main/java/matcher/jobs/ImportMatchesJob.java new file mode 100644 index 00000000..44a3ffc0 --- /dev/null +++ b/src/main/java/matcher/jobs/ImportMatchesJob.java @@ -0,0 +1,143 @@ +package matcher.jobs; + +import java.io.DataInputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.function.DoubleConsumer; + +import job4j.JobState; + +import matcher.Matcher; +import matcher.config.Config; +import matcher.config.UidConfig; +import matcher.type.ClassEnv; +import matcher.type.ClassEnvironment; +import matcher.type.ClassInstance; +import matcher.type.FieldInstance; +import matcher.type.MatchableKind; +import matcher.type.MethodInstance; +import matcher.type.MethodVarInstance; + +public class ImportMatchesJob extends MatcherJob { + public ImportMatchesJob(Matcher matcher) { + super(JobCategories.IMPORT_MATCHES); + + this.matcher = matcher; + } + + @Override + protected Boolean execute(DoubleConsumer progressReceiver) { + importMatches(progressReceiver); + return importedAny; + } + + private void importMatches(DoubleConsumer progressReceiver) { + UidConfig config = Config.getUidConfig(); + if (!config.isValid()) return; + + try { + HttpURLConnection conn = (HttpURLConnection) new URL("https", + config.getAddress().getHostString(), + config.getAddress().getPort(), + String.format("/%s/matches/%s/%s", config.getProject(), config.getVersionA(), config.getVersionB())).openConnection(); + conn.setRequestProperty("X-Token", config.getToken()); + + progressReceiver.accept(0.5); + + try (DataInputStream is = new DataInputStream(conn.getInputStream())) { + ClassEnvironment env = matcher.getEnv(); + int typeOrdinal; + MatchableKind type; + + while ((typeOrdinal = is.read()) != -1) { + if (state == JobState.CANCELING) { + break; + } + + type = MatchableKind.VALUES[typeOrdinal]; + int uid = is.readInt(); + String idA = is.readUTF(); + String idB = is.readUTF(); + + ClassInstance clsA = getCls(env.getEnvA(), idA, type); + ClassInstance clsB = getCls(env.getEnvB(), idB, type); + if (clsA == null || clsB == null) continue; + + switch (type) { + case CLASS: + matcher.match(clsA, clsB); + importedAny = true; + break; + case METHOD: + case METHOD_ARG: + case METHOD_VAR: { + MethodInstance methodA = getMethod(clsA, idA, type); + MethodInstance methodB = getMethod(clsB, idB, type); + if (methodA == null || methodB == null) break; + + if (type == MatchableKind.METHOD) { + matcher.match(methodA, methodB); + importedAny = true; + } else { + idA = idA.substring(idA.lastIndexOf(')') + 1); + idB = idB.substring(idB.lastIndexOf(')') + 1); + + MethodVarInstance varA = methodA.getVar(idA, type == MatchableKind.METHOD_ARG); + MethodVarInstance varB = methodB.getVar(idB, type == MatchableKind.METHOD_ARG); + + if (varA != null && varB != null) { + matcher.match(varA, varB); + importedAny = true; + } + } + + break; + } + case FIELD: { + FieldInstance fieldA = getField(clsA, idA); + FieldInstance fieldB = getField(clsB, idB); + if (fieldA == null || fieldB == null) break; + + matcher.match(fieldA, fieldB); + importedAny = true; + break; + } + } + } + } + + progressReceiver.accept(1); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private ClassInstance getCls(ClassEnv env, String fullId, MatchableKind type) { + if (type == MatchableKind.CLASS) { + return env.getLocalClsById(fullId); + } else if (type == MatchableKind.FIELD) { + int pos = fullId.lastIndexOf('/', fullId.lastIndexOf(";;") - 2); + + return env.getLocalClsById(fullId.substring(0, pos)); + } else { + int pos = fullId.lastIndexOf('/', fullId.lastIndexOf('(') - 1); + + return env.getLocalClsById(fullId.substring(0, pos)); + } + } + + private MethodInstance getMethod(ClassInstance cls, String fullId, MatchableKind type) { + int end = type == MatchableKind.METHOD ? fullId.length() : fullId.lastIndexOf(')') + 1; + + return cls.getMethod(fullId.substring(fullId.lastIndexOf('/', fullId.lastIndexOf('(', end - 1) - 1) + 1, end)); + } + + private FieldInstance getField(ClassInstance cls, String fullId) { + return cls.getField(fullId.substring(fullId.lastIndexOf('/', fullId.lastIndexOf(";;") - 2) + 1)); + } + + private final Matcher matcher; + private boolean importedAny; +} diff --git a/src/main/java/matcher/jobs/JobCategories.java b/src/main/java/matcher/jobs/JobCategories.java new file mode 100644 index 00000000..c7a51c06 --- /dev/null +++ b/src/main/java/matcher/jobs/JobCategories.java @@ -0,0 +1,36 @@ +package matcher.jobs; + +import job4j.JobCategory; + +public class JobCategories { + public static final JobCategory INIT_MATCHER = new JobCategory("init-matcher"); + public static final JobCategory LOAD_PROJECT = new JobCategory("load-project", INIT_MATCHER); + public static final JobCategory INIT_ENV = new JobCategory("init-env", LOAD_PROJECT); + public static final JobCategory MATCH_UNOBFUSCATED = new JobCategory("match-unobfuscated", LOAD_PROJECT); + public static final JobCategory NETWORK_SYNC = new JobCategory("network-sync"); + + public static final JobCategory IMPORT_MATCHES = new JobCategory("import-matches"); + public static final JobCategory RANK_MATCHES = new JobCategory("rank-matches"); + public static final JobCategory SUBMIT_MATCHES = new JobCategory("submit-matches", NETWORK_SYNC); + + public static final JobCategory DECOMPILE = new JobCategory("decompile"); + public static final JobCategory DECOMPILE_SOURCE = new JobCategory("decompile-source", DECOMPILE); + public static final JobCategory DECOMPILE_DEST = new JobCategory("decompile-dest", DECOMPILE); + + public static final JobCategory AUTOMATCH = new JobCategory("automatch"); + + public static final JobCategory AUTOMATCH_ALL = new JobCategory("automatch-all", AUTOMATCH); + public static final JobCategory AUTOMATCH_ALL_INTERMEDIATE = new JobCategory("automatch-all:intermediate", AUTOMATCH_ALL); + public static final JobCategory AUTOMATCH_ALL_FULL = new JobCategory("automatch-all:full", AUTOMATCH_ALL); + public static final JobCategory AUTOMATCH_ALL_EXTRA = new JobCategory("automatch-all:extra", AUTOMATCH_ALL); + public static final JobCategory AUTOMATCH_ALL_LOCALS = new JobCategory("automatch-all:locals", AUTOMATCH_ALL); + + public static final JobCategory AUTOMATCH_CLASSES = new JobCategory("automatch-classes", AUTOMATCH); + public static final JobCategory AUTOMATCH_FIELDS = new JobCategory("automatch-fields", AUTOMATCH); + public static final JobCategory AUTOMATCH_METHODS = new JobCategory("automatch-methods", AUTOMATCH); + public static final JobCategory AUTOMATCH_LOCALS = new JobCategory("automatch-locals", AUTOMATCH); + public static final JobCategory AUTOMATCH_METHOD_ARGS = new JobCategory("automatch-locals:method-args", AUTOMATCH_LOCALS); + public static final JobCategory AUTOMATCH_METHOD_VARS = new JobCategory("automatch-locals:method-vars", AUTOMATCH_LOCALS); + + public static final JobCategory PROPAGATE_METHOD_NAMES = new JobCategory("propagate-method-names"); +} diff --git a/src/main/java/matcher/jobs/MatcherJob.java b/src/main/java/matcher/jobs/MatcherJob.java new file mode 100644 index 00000000..682eb3fd --- /dev/null +++ b/src/main/java/matcher/jobs/MatcherJob.java @@ -0,0 +1,20 @@ +package matcher.jobs; + +import job4j.Job; +import job4j.JobCategory; + +public abstract class MatcherJob extends Job { + public MatcherJob(JobCategory category) { + super(category); + init(); + } + + public MatcherJob(JobCategory category, String id) { + super(category, id); + init(); + } + + private void init() { + addBlockedBy(JobCategories.INIT_MATCHER); + } +} diff --git a/src/main/java/matcher/jobs/RankMatchResultsJob.java b/src/main/java/matcher/jobs/RankMatchResultsJob.java new file mode 100644 index 00000000..cbea196e --- /dev/null +++ b/src/main/java/matcher/jobs/RankMatchResultsJob.java @@ -0,0 +1,55 @@ +package matcher.jobs; + +import java.util.List; +import java.util.function.DoubleConsumer; + +import matcher.classifier.ClassClassifier; +import matcher.classifier.ClassifierLevel; +import matcher.classifier.FieldClassifier; +import matcher.classifier.MethodClassifier; +import matcher.classifier.MethodVarClassifier; +import matcher.classifier.RankResult; +import matcher.type.ClassEnvironment; +import matcher.type.ClassInstance; +import matcher.type.FieldInstance; +import matcher.type.Matchable; +import matcher.type.MethodInstance; +import matcher.type.MethodVarInstance; + +public class RankMatchResultsJob extends MatcherJob>>> { + public RankMatchResultsJob(ClassEnvironment env, ClassifierLevel matchLevel, Matchable selection, List cmpClasses) { + super(JobCategories.RANK_MATCHES); + + this.env = env; + this.selection = selection; + this.matchLevel = matchLevel; + this.cmpClasses = cmpClasses; + } + + @Override + protected List>> execute(DoubleConsumer progressReceiver) { + if (selection instanceof ClassInstance) { // unmatched class or no member/method var selected + ClassInstance cls = (ClassInstance) selection; + return ClassClassifier.rankParallel(cls, cmpClasses.toArray(new ClassInstance[0]), matchLevel, env, MAX_MISMATCH); + } else if (selection instanceof MethodInstance) { // unmatched method or no method var selected + MethodInstance method = (MethodInstance) selection; + return MethodClassifier.rank(method, method.getCls().getMatch().getMethods(), matchLevel, env, MAX_MISMATCH); + } else if (selection instanceof FieldInstance) { // field + FieldInstance field = (FieldInstance) selection; + return FieldClassifier.rank(field, field.getCls().getMatch().getFields(), matchLevel, env, MAX_MISMATCH); + } else if (selection instanceof MethodVarInstance) { // method arg/var + MethodVarInstance var = (MethodVarInstance) selection; + MethodInstance cmpMethod = var.getMethod().getMatch(); + MethodVarInstance[] cmp = var.isArg() ? cmpMethod.getArgs() : cmpMethod.getVars(); + return MethodVarClassifier.rank(var, cmp, matchLevel, env, MAX_MISMATCH); + } else { + throw new IllegalStateException(); + } + } + + public static final double MAX_MISMATCH = Double.POSITIVE_INFINITY; + private final ClassEnvironment env; + private final Matchable selection; + private final ClassifierLevel matchLevel; + private final List cmpClasses; +} diff --git a/src/main/java/matcher/jobs/SubmitMatchesJob.java b/src/main/java/matcher/jobs/SubmitMatchesJob.java new file mode 100644 index 00000000..6a73b861 --- /dev/null +++ b/src/main/java/matcher/jobs/SubmitMatchesJob.java @@ -0,0 +1,115 @@ +package matcher.jobs; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.function.DoubleConsumer; + +import job4j.JobState; + +import matcher.Matcher; +import matcher.config.Config; +import matcher.config.UidConfig; +import matcher.type.ClassInstance; +import matcher.type.FieldInstance; +import matcher.type.Matchable; +import matcher.type.MatchableKind; +import matcher.type.MethodInstance; +import matcher.type.MethodVarInstance; + +public class SubmitMatchesJob extends MatcherJob { + public SubmitMatchesJob(Matcher matcher) { + super(JobCategories.SUBMIT_MATCHES); + + this.matcher = matcher; + } + + @Override + protected Void execute(DoubleConsumer progressReceiver) { + submitMatches(progressReceiver); + return null; + } + + private void submitMatches(DoubleConsumer progressReceiver) { + UidConfig config = Config.getUidConfig(); + if (!config.isValid()) return; + + try { + HttpURLConnection conn = (HttpURLConnection) new URL("https", + config.getAddress().getHostString(), + config.getAddress().getPort(), + String.format("/%s/link/%s/%s", config.getProject(), config.getVersionA(), config.getVersionB())).openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("X-Token", config.getToken()); + conn.setDoOutput(true); + + List> requested = new ArrayList<>(); + + try (DataOutputStream os = new DataOutputStream(conn.getOutputStream())) { + for (ClassInstance cls : matcher.getEnv().getClassesA()) { + if (state == JobState.CANCELING) { + break; + } + + if (!cls.hasMatch() || !cls.isInput()) continue; // TODO: skip with known + matched uids + + assert cls.getMatch() != cls; + + requested.add(cls); + os.writeByte(MatchableKind.CLASS.ordinal()); + os.writeUTF(cls.getId()); + os.writeUTF(cls.getMatch().getId()); + + for (MethodInstance method : cls.getMethods()) { + if (!method.hasMatch() || !method.isReal()) continue; + + String srcMethodId = cls.getId()+"/"+method.getId(); + String dstMethodId = cls.getMatch().getId()+"/"+method.getMatch().getId(); + + requested.add(method); + os.writeByte(MatchableKind.METHOD.ordinal()); + os.writeUTF(srcMethodId); + os.writeUTF(dstMethodId); + + for (MethodVarInstance arg : method.getArgs()) { + if (!arg.hasMatch()) continue; + + requested.add(arg); + os.writeByte(MatchableKind.METHOD_ARG.ordinal()); + os.writeUTF(srcMethodId+arg.getId()); + os.writeUTF(dstMethodId+arg.getMatch().getId()); + } + } + + for (FieldInstance field : cls.getFields()) { + if (!field.hasMatch() || !field.isReal()) continue; + + requested.add(field); + os.writeByte(MatchableKind.FIELD.ordinal()); + os.writeUTF(cls.getId()+"/"+field.getId()); + os.writeUTF(cls.getMatch().getId()+"/"+field.getMatch().getId()); + } + } + } + + progressReceiver.accept(0.5); + + try (DataInputStream is = new DataInputStream(conn.getInputStream())) { + for (Matchable matchable : requested) { + int uid = is.readInt(); + } + } + + progressReceiver.accept(1); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private final Matcher matcher; +} diff --git a/src/main/java/matcher/serdes/MatchesIo.java b/src/main/java/matcher/serdes/MatchesIo.java index 01b58a75..1e34e653 100644 --- a/src/main/java/matcher/serdes/MatchesIo.java +++ b/src/main/java/matcher/serdes/MatchesIo.java @@ -13,7 +13,6 @@ import java.util.Base64; import java.util.Comparator; import java.util.List; -import java.util.function.DoubleConsumer; import matcher.Matcher; import matcher.config.Config; @@ -28,7 +27,7 @@ import matcher.type.MethodVarInstance; public class MatchesIo { - public static void read(Path path, List inputDirs, boolean verifyInputs, Matcher matcher, DoubleConsumer progressReceiver) { + public static void read(Path path, List inputDirs, boolean verifyInputs, Matcher matcher) { ClassEnvironment env = matcher.getEnv(); try (BufferedReader reader = Files.newBufferedReader(path)) { @@ -152,8 +151,7 @@ public static void read(Path path, List inputDirs, boolean verifyInputs, M if (inputDirs != null) { matcher.initFromMatches(inputDirs, inputFilesA, inputFilesB, cpFiles, cpFilesA, cpFilesB, - nonObfuscatedClassPatternA, nonObfuscatedClassPatternB, nonObfuscatedMemberPatternA, nonObfuscatedMemberPatternB, - progressReceiver); + nonObfuscatedClassPatternA, nonObfuscatedClassPatternB, nonObfuscatedMemberPatternA, nonObfuscatedMemberPatternB); inputDirs = null; } } diff --git a/src/main/java/matcher/type/ClassEnvironment.java b/src/main/java/matcher/type/ClassEnvironment.java index edf79759..057dc4b5 100644 --- a/src/main/java/matcher/type/ClassEnvironment.java +++ b/src/main/java/matcher/type/ClassEnvironment.java @@ -23,6 +23,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.DoubleConsumer; import java.util.regex.Pattern; @@ -43,7 +44,7 @@ import matcher.type.Signature.ClassSignature; public final class ClassEnvironment implements ClassEnv { - public void init(ProjectConfig config, DoubleConsumer progressReceiver) { + public void init(ProjectConfig config, DoubleConsumer progressReceiver, AtomicBoolean cancelListener) { final double cpInitCost = 0.05; final double classReadCost = 0.2; double progress = 0; @@ -56,9 +57,13 @@ public void init(ProjectConfig config, DoubleConsumer progressReceiver) { try { for (int i = 0; i < 2; i++) { + if (cancelListener.get()) { + return; + } + if ((i == 0) != inputsBeforeClassPath) { // class path indexing - initClassPath(config.getSharedClassPath(), inputsBeforeClassPath); + initClassPath(config.getSharedClassPath(), inputsBeforeClassPath, cancelListener); CompletableFuture.allOf( CompletableFuture.runAsync(() -> extractorA.processClassPath(config.getClassPathA(), inputsBeforeClassPath)), CompletableFuture.runAsync(() -> extractorB.processClassPath(config.getClassPathB(), inputsBeforeClassPath))).get(); @@ -91,8 +96,12 @@ public void init(ProjectConfig config, DoubleConsumer progressReceiver) { progressReceiver.accept(1); } - private void initClassPath(Collection sharedClassPath, boolean checkExisting) throws IOException { + private void initClassPath(Collection sharedClassPath, boolean checkExisting, AtomicBoolean cancelListener) throws IOException { for (Path archive : sharedClassPath) { + if (cancelListener.get()) { + return; + } + cpFiles.add(new InputFile(archive)); FileSystem fs = Util.iterateJar(archive, false, file -> { diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 3ac85dbe..04814d89 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -10,6 +10,7 @@ exports matcher; exports matcher.bcremap; exports matcher.serdes; + exports job4j; requires transitive org.slf4j; requires cfr; @@ -20,6 +21,7 @@ requires transitive javafx.controls; requires transitive javafx.graphics; requires transitive javafx.web; + requires transitive org.controlsfx.controls; requires transitive org.objectweb.asm; requires transitive org.objectweb.asm.tree; requires org.objectweb.asm.commons;