package org.scribble.cli; import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.stream.Collectors; import org.scribble.ast.Module; import org.scribble.ast.ProtocolDecl; import org.scribble.ast.global.GProtocolDecl; import org.scribble.main.Job; import org.scribble.main.JobContext; import org.scribble.main.MainContext; import org.scribble.main.RuntimeScribbleException; import org.scribble.main.ScribbleException; import org.scribble.main.resource.DirectoryResourceLocator; import org.scribble.main.resource.ResourceLocator; import org.scribble.model.endpoint.EGraph; import org.scribble.model.global.SGraph; import org.scribble.sesstype.name.GProtocolName; import org.scribble.sesstype.name.LProtocolName; import org.scribble.sesstype.name.Role; import org.scribble.util.ScribParserException; import org.scribble.util.ScribUtil; public class CommandLine { protected enum ArgFlag { // Unique flags JUNIT, // For internal use (JUnit test harness) MAIN_MOD, IMPORT_PATH, VERBOSE, SCHAN_API_SUBTYPES, OLD_WF, NO_LIVENESS, LTSCONVERT_MIN, // Currently only affects EFSM output (i.e. -fsm..) and API gen -- doesn't affect validation FAIR, NO_LOCAL_CHOICE_SUBJECT_CHECK, NO_ACCEPT_CORRELATION_CHECK, DOT, AUT, NO_VALIDATION, INLINE_MAIN_MOD, F17, // Non-unique flags PROJECT, API_OUTPUT, EFSM, VALIDATION_EFSM, UNFAIR_EFSM, UNFAIR_EFSM_PNG, EFSM_PNG, VALIDATION_EFSM_PNG, SGRAPH, UNFAIR_SGRAPH, SGRAPH_PNG, UNFAIR_SGRAPH_PNG, API_GEN, SESS_API_GEN, SCHAN_API_GEN, } private final Map<ArgFlag, String[]> args; // Maps each flag to list of associated argument values public CommandLine(String... args) throws CommandLineException { this.args = new CommandLineArgParser(args).getArgs(); if (!this.args.containsKey(ArgFlag.MAIN_MOD) && !this.args.containsKey(ArgFlag.INLINE_MAIN_MOD)) { throw new CommandLineException("No main module has been specified\r\n"); } } public static void main(String[] args) throws CommandLineException, ScribbleException { try { new CommandLine(args).run(); } catch (ScribParserException | CommandLineException e) { System.err.println(e.getMessage()); // No need to give full stack trace, even for debug, for command line errors System.exit(1); } catch (RuntimeScribbleException e) { System.err.println(e.getMessage()); System.exit(1); } } public void run() throws ScribbleException, CommandLineException, ScribParserException { try { Job job = newJob(newMainContext()); ScribbleException fail = null; try { /*// Scribble extensions (custom Job passes) if (this.args.containsKey(ArgFlag.F17)) { GProtocolName simpname = new GProtocolName(this.args.get(ArgFlag.F17)[0]); F17Main.parseAndCheckWF(job, simpname); // Includes base passes } // Base Scribble else*/ { job.checkWellFormedness(); } } catch (ScribbleException x) { fail = x; } try { // Following must be ordered appropriately -- ? if (this.args.containsKey(ArgFlag.PROJECT)) { outputProjections(job); } if (this.args.containsKey(ArgFlag.EFSM)) { outputEGraph(job, true, true); } if (this.args.containsKey(ArgFlag.VALIDATION_EFSM)) { outputEGraph(job, false, true); } if (this.args.containsKey(ArgFlag.UNFAIR_EFSM)) { outputEGraph(job, false, false); } if (this.args.containsKey(ArgFlag.EFSM_PNG)) { drawEGraph(job, true, true); } if (this.args.containsKey(ArgFlag.VALIDATION_EFSM_PNG)) { drawEGraph(job, false, true); } if (this.args.containsKey(ArgFlag.UNFAIR_EFSM_PNG)) { drawEGraph(job, false, false); } if (this.args.containsKey(ArgFlag.SGRAPH) || this.args.containsKey(ArgFlag.SGRAPH_PNG) || this.args.containsKey(ArgFlag.UNFAIR_SGRAPH) || this.args.containsKey(ArgFlag.UNFAIR_SGRAPH_PNG)) { if (job.useOldWf) { throw new CommandLineException("Global model flag(s) incompatible with: " + CommandLineArgParser.OLD_WF_FLAG); } if (this.args.containsKey(ArgFlag.SGRAPH)) { outputSGraph(job, true); } if (this.args.containsKey(ArgFlag.UNFAIR_SGRAPH)) { outputSGraph(job, false); } if (this.args.containsKey(ArgFlag.SGRAPH_PNG)) { drawSGraph(job, true); } if (this.args.containsKey(ArgFlag.UNFAIR_SGRAPH_PNG)) { drawSGraph(job, false); } } } catch (ScribbleException x) { if (fail == null) { fail = x; } } if (fail != null) { throw fail; } if (this.args.containsKey(ArgFlag.SESS_API_GEN)) { outputSessionApi(job); } if (this.args.containsKey(ArgFlag.SCHAN_API_GEN)) { outputStateChannelApi(job); } if (this.args.containsKey(ArgFlag.API_GEN)) { outputEndpointApi(job); } } catch (ScribbleException e) // Wouldn't need to do this if not Runnable (so maybe change) { if (this.args.containsKey(ArgFlag.JUNIT) || this.args.containsKey(ArgFlag.VERBOSE)) { /*RuntimeScribbleException ee = new RuntimeScribbleException(e.getMessage()); ee.setStackTrace(e.getStackTrace()); throw ee;*/ throw e; } else { System.err.println(e.getMessage()); // JUnit harness looks for an exception System.exit(1); } } } // FIXME: option to write to file, like classes private void outputProjections(Job job) throws CommandLineException, ScribbleException { JobContext jcontext = job.getContext(); String[] args = this.args.get(ArgFlag.PROJECT); for (int i = 0; i < args.length; i += 2) { GProtocolName fullname = checkGlobalProtocolArg(jcontext, args[i]); Role role = checkRoleArg(jcontext, fullname, args[i+1]); Map<LProtocolName, Module> projections = job.getProjections(fullname, role); System.out.println("\n" + projections.values().stream().map((p) -> p.toString()).collect(Collectors.joining("\n\n"))); } } // dot/aut text output // forUser: true means for API gen and general user info (may be minimised), false means for validation (non-minimised, fair or unfair) // (forUser && !fair) should not hold, i.e. unfair doesn't make sense if forUser private void outputEGraph(Job job, boolean forUser, boolean fair) throws ScribbleException, CommandLineException { JobContext jcontext = job.getContext(); String[] args = forUser ? this.args.get(ArgFlag.EFSM) : (fair ? this.args.get(ArgFlag.VALIDATION_EFSM) : this.args.get(ArgFlag.UNFAIR_EFSM)); for (int i = 0; i < args.length; i += 2) { GProtocolName fullname = checkGlobalProtocolArg(jcontext, args[i]); Role role = checkRoleArg(jcontext, fullname, args[i+1]); EGraph fsm = getEGraph(job, fullname, role, forUser, fair); String out = this.args.containsKey(ArgFlag.AUT) ? fsm.toAut() : fsm.toDot(); System.out.println("\n" + out); // Endpoint graphs are "inlined" (a single graph is built) } } private void drawEGraph(Job job, boolean forUser, boolean fair) throws ScribbleException, CommandLineException { JobContext jcontext = job.getContext(); String[] args = forUser ? this.args.get(ArgFlag.EFSM_PNG) : (fair ? this.args.get(ArgFlag.VALIDATION_EFSM_PNG) : this.args.get(ArgFlag.UNFAIR_EFSM_PNG)); for (int i = 0; i < args.length; i += 3) { GProtocolName fullname = checkGlobalProtocolArg(jcontext, args[i]); Role role = checkRoleArg(jcontext, fullname, args[i+1]); String png = args[i+2]; EGraph fsm = getEGraph(job, fullname, role, forUser, fair); runDot(fsm.toDot(), png); } } // Endpoint graphs are "inlined", so only a single graph is built (cf. projection output) private EGraph getEGraph(Job job, GProtocolName fullname, Role role, boolean forUser, boolean fair) throws ScribbleException, CommandLineException { JobContext jcontext = job.getContext(); GProtocolDecl gpd = (GProtocolDecl) jcontext.getMainModule().getProtocolDecl(fullname.getSimpleName()); if (gpd == null || !gpd.header.roledecls.getRoles().contains(role)) { throw new CommandLineException("Bad FSM construction args: " + Arrays.toString(this.args.get(ArgFlag.DOT))); } EGraph graph; if (forUser) // The (possibly minimised) user-output EFSM for API gen { graph = this.args.containsKey(ArgFlag.LTSCONVERT_MIN) ? jcontext.getMinimisedEGraph(fullname, role) : jcontext.getEGraph(fullname, role); } else // The (possibly unfair-transformed) internal EFSM for validation { graph = //(!this.args.containsKey(ArgFlag.FAIR) && !this.args.containsKey(ArgFlag.NO_LIVENESS)) // Cf. GlobalModelChecker.getEndpointFSMs !fair ? jcontext.getUnfairEGraph(fullname, role) : jcontext.getEGraph(fullname, role); } if (graph == null) { throw new RuntimeScribbleException("Shouldn't see this: " + fullname); // Should be suppressed by an earlier failure } return graph; } private void outputSGraph(Job job, boolean fair) throws ScribbleException, CommandLineException { JobContext jcontext = job.getContext(); String[] args = fair ? this.args.get(ArgFlag.SGRAPH) : this.args.get(ArgFlag.UNFAIR_SGRAPH); for (int i = 0; i < args.length; i += 1) { GProtocolName fullname = checkGlobalProtocolArg(jcontext, args[i]); SGraph model = getSGraph(job, fullname, fair); String out = this.args.containsKey(ArgFlag.AUT) ? model.toAut() : model.toDot(); System.out.println("\n" + out); } } private void drawSGraph(Job job, boolean fair) throws ScribbleException, CommandLineException { JobContext jcontext = job.getContext(); String[] args = fair ? this.args.get(ArgFlag.SGRAPH_PNG) : this.args.get(ArgFlag.UNFAIR_SGRAPH_PNG); for (int i = 0; i < args.length; i += 2) { GProtocolName fullname = checkGlobalProtocolArg(jcontext, args[i]); String png = args[i+1]; SGraph model = getSGraph(job, fullname, fair); runDot(model.toDot(), png); } } private static SGraph getSGraph(Job job, GProtocolName fullname, boolean fair) throws ScribbleException { JobContext jcontext = job.getContext(); SGraph model = fair ? jcontext.getSGraph(fullname) : jcontext.getUnfairSGraph(fullname); if (model == null) { throw new RuntimeScribbleException("Shouldn't see this: " + fullname); // Should be suppressed by an earlier failure } return model; } private void outputEndpointApi(Job job) throws ScribbleException, CommandLineException { JobContext jcontext = job.getContext(); String[] args = this.args.get(ArgFlag.API_GEN); for (int i = 0; i < args.length; i += 2) { GProtocolName fullname = checkGlobalProtocolArg(jcontext, args[i]); Map<String, String> sessClasses = job.generateSessionApi(fullname); outputClasses(sessClasses); Role role = checkRoleArg(jcontext, fullname, args[i+1]); Map<String, String> scClasses = job.generateStateChannelApi(fullname, role, this.args.containsKey(ArgFlag.SCHAN_API_SUBTYPES)); outputClasses(scClasses); } } private void outputSessionApi(Job job) throws ScribbleException, CommandLineException { JobContext jcontext = job.getContext(); String[] args = this.args.get(ArgFlag.SESS_API_GEN); for (String fullname : args) { GProtocolName gpn = checkGlobalProtocolArg(jcontext, fullname); Map<String, String> classes = job.generateSessionApi(gpn); outputClasses(classes); } } private void outputStateChannelApi(Job job) throws ScribbleException, CommandLineException { JobContext jcontext = job.getContext(); String[] args = this.args.get(ArgFlag.SCHAN_API_GEN); for (int i = 0; i < args.length; i += 2) { GProtocolName fullname = checkGlobalProtocolArg(jcontext, args[i]); Role role = checkRoleArg(jcontext, fullname, args[i+1]); Map<String, String> classes = job.generateStateChannelApi(fullname, role, this.args.containsKey(ArgFlag.SCHAN_API_SUBTYPES)); outputClasses(classes); } } // filepath -> class source private void outputClasses(Map<String, String> classes) throws ScribbleException { Consumer<String> f; if (this.args.containsKey(ArgFlag.API_OUTPUT)) { String dir = this.args.get(ArgFlag.API_OUTPUT)[0]; f = (path) -> { ScribUtil.handleLambdaScribbleException(() -> { String tmp = dir + "/" + path; if (this.args.containsKey(ArgFlag.VERBOSE)) { System.out.println("\n[DEBUG] Writing to: " + tmp); } ScribUtil.writeToFile(tmp, classes.get(path)); return null; }); }; } else { f = (path) -> { System.out.println(path + ":\n" + classes.get(path)); }; } classes.keySet().stream().forEach(f); } private static void runDot(String dot, String png) throws ScribbleException, CommandLineException { String tmpName = png + ".tmp"; File tmp = new File(tmpName); if (tmp.exists()) { throw new CommandLineException("Cannot overwrite: " + tmpName); } try { ScribUtil.writeToFile(tmpName, dot); String[] res = ScribUtil.runProcess("dot", "-Tpng", "-o" + png, tmpName); System.out.print(!res[1].isEmpty() ? res[1] : res[0]); // already "\n" terminated } finally { tmp.delete(); } } private Job newJob(MainContext mc) { //Job job = new Job(cjob); // Doesn't work due to (recursive) maven dependencies return mc.newJob(); } private MainContext newMainContext() throws ScribParserException, ScribbleException { //boolean jUnit = this.args.containsKey(ArgFlag.JUNIT); boolean debug = this.args.containsKey(ArgFlag.VERBOSE); boolean useOldWF = this.args.containsKey(ArgFlag.OLD_WF); boolean noLiveness = this.args.containsKey(ArgFlag.NO_LIVENESS); boolean minEfsm = this.args.containsKey(ArgFlag.LTSCONVERT_MIN); boolean fair = this.args.containsKey(ArgFlag.FAIR); boolean noLocalChoiceSubjectCheck = this.args.containsKey(ArgFlag.NO_LOCAL_CHOICE_SUBJECT_CHECK); boolean noAcceptCorrelationCheck = this.args.containsKey(ArgFlag.NO_ACCEPT_CORRELATION_CHECK); boolean noValidation = this.args.containsKey(ArgFlag.NO_VALIDATION); boolean f17 = this.args.containsKey(ArgFlag.F17); List<Path> impaths = this.args.containsKey(ArgFlag.IMPORT_PATH) ? CommandLine.parseImportPaths(this.args.get(ArgFlag.IMPORT_PATH)[0]) : Collections.emptyList(); ResourceLocator locator = new DirectoryResourceLocator(impaths); if (this.args.containsKey(ArgFlag.INLINE_MAIN_MOD)) { return new MainContext(debug, locator, this.args.get(ArgFlag.INLINE_MAIN_MOD)[0], useOldWF, noLiveness, minEfsm, fair, noLocalChoiceSubjectCheck, noAcceptCorrelationCheck, noValidation, f17); } else { Path mainpath = CommandLine.parseMainPath(this.args.get(ArgFlag.MAIN_MOD)[0]); //return new MainContext(jUnit, debug, locator, mainpath, useOldWF, noLiveness); return new MainContext(debug, locator, mainpath, useOldWF, noLiveness, minEfsm, fair, noLocalChoiceSubjectCheck, noAcceptCorrelationCheck, noValidation, f17); } } private static Path parseMainPath(String path) { return Paths.get(path); } private static List<Path> parseImportPaths(String paths) { return Arrays.stream(paths.split(File.pathSeparator)).map((s) -> Paths.get(s)).collect(Collectors.toList()); } private static GProtocolName checkGlobalProtocolArg(JobContext jcontext, String simpname) throws CommandLineException { GProtocolName simpgpn = new GProtocolName(simpname); Module main = jcontext.getMainModule(); if (!main.hasProtocolDecl(simpgpn)) { throw new CommandLineException("Global protocol not found: " + simpname); } ProtocolDecl<?> pd = main.getProtocolDecl(simpgpn); if (pd == null || !pd.isGlobal()) { throw new CommandLineException("Global protocol not found: " + simpname); } if (pd.isAuxModifier()) // CHECKME: maybe don't check for all, e.g. -project { throw new CommandLineException("Invalid aux protocol specified as root: " + simpname); } return new GProtocolName(jcontext.main, simpgpn); } private static Role checkRoleArg(JobContext jcontext, GProtocolName fullname, String rolename) throws CommandLineException { ProtocolDecl<?> pd = jcontext.getMainModule().getProtocolDecl(fullname.getSimpleName()); Role role = new Role(rolename); if (!pd.header.roledecls.getRoles().contains(role)) { throw new CommandLineException("Role not declared for " + fullname + ": " + role); } return role; } }