TeamcenterKnowledge

Skills

Cameo Headless Automation

Skill cameo-headless-automation. Run scripts inside Cameo / MagicDraw without touching the GUI - a custom CommandLineAction plugin driven by CommandLineActionLauncher, or a persistent worker that serves HTTP so calls cost 0.4s instead of 40s. Covers the three things that make an unattended run fail while looking healthy - batch mode still needs a desktop, the FL_* licence properties suppress the FlexNet dialog in BATCH but not in GUI, and a batch process discards an unsaved edit while the create still reports success. Use for any headless/batch/unattended Cameo work, macro automation without clicking Tools > Macros, or a "the action ran and nothing happened" investigation.

Verified live 2026-08-12 on Cameo Enterprise Architecture 2024x Refresh3 (2024.3.0-55-260588bf), bundled Temurin JDK 17.0.14, Open API version 1.2, FlexNet floating licence 1101@localhost, edition Enterprise. Vendor source: the Open API Developer Guide shipped in the install at <install>\openapi\guide\guide\ (extract openapi\guide.zip if absent) and the worked example at <install>\openapi\examples\commandlineplugin.

Working implementation: cameo-mcp in this workspace (plugin + MCP server + tests).


The mechanism

Cameo runs a batch program through com.nomagic.magicdraw.commandline.CommandLineActionLauncher. Which code it runs is chosen by a system property naming a class that a loaded plugin registered:

-Dcom.nomagic.magicdraw.commandline.action=<your.FQCN>
  com.nomagic.magicdraw.commandline.CommandLineActionLauncher  arg1=v1 arg2=v2

Your plugin registers it in init():

CommandLineActionManager.getInstance().addAction(new MyAction());

Two base types, both verified present on this build with javap:

Type Use
CommandLineAction (interface, byte execute(String[])) you manage projects yourself, or need none
ProjectCommandLineAction (abstract, byte execute(String[], Properties, Project)) the launcher opens the project(s) and calls you once per project

ProjectCommandLineAction is the one to reach for: it gives you project=, server=, username=, password=, encryptPassword=, serverType=, enableSSL=, projectPassword=, version=, branch=, properties= for free, so the same action reaches a Teamwork Cloud or 3DEXPERIENCE project and not just a local .mdzip. Passwords are expected encrypted; generate one by running any launcher with the single argument generateServerPassword=yourPassword.

Make the action generic once. Rather than a plugin per job, write one action that evaluates a script file and stop recompiling: the Automaton plugin puts Jython 2.7.4, Groovy 4, Rhino and BeanShell on the shared plugin classloader, so new ScriptEngineManager(getClass().getClassLoader()).getEngineByName("python") resolves — provided your plugin.xml declares <required-plugin id="AutomatonPlugin" name="Automaton"/> so load order is right. Verified engine list from a live instance: jython 2.7.4 [python, jython], Groovy Scripting Engine 2.0, JavaScript Rhino 1.7.13, BeanShell Engine 2.1.7.

Classpath and launch

Compile with the tool's own bundled JDK (<install>\jre\bin\javac.exe) against <install>\lib\* plus every subdirectory of lib — the Developer Guide's Compilation classpath page, with brand.jar / brand_api.jar first when they exist. Then the bytecode level cannot drift from the JVM that loads it.

At run time the app's own classpath is one manifest jar:

-cp <install>\lib\classpath.jar
-Desi.system.config=<install>\data\application.conf     (mandatory)
@<install>\bin\vm.options                                (the --add-opens set)
-Dfile.encoding=UTF-8

Deploying without installing: -Dmd.plugins.dir="<install>\plugins;<your dir>" adds an extra plugins directory (each holding <plugin-id>\{plugin.xml,*.jar}). This is how you test against an install you do not want to modify. Plugins also load from <config location>\plugins, but that is not equivalent for batch: the interactive launcher sets -DLOCALCONFIG, a bare java ... Launcher command line does not, so a plugin living only there loads in the GUI and silently does not load in batch.


The three things that make it fail while looking fine

1. Batch mode is NOT headless

"The application cannot be run on the headless device even in a batch mode. The graphical environment is required." — Developer Guide, Running programs in batch mode

It runs with no window and no user, but it needs a logged-in desktop session. It will not run as a Windows service, nor over an RDP session that was disconnected rather than left open. Do not promise unattended-server operation. (Whether it survives a locked workstation is untested here.)

2. The FL_* licence properties work in BATCH but NOT in GUI

-DFL_FORCE_USAGE=true -DFL_SERVER_ADDRESS=localhost -DFL_SERVER_PORT=1101 -DFL_EDITION=Enterprise

EXERCISED both ways in one sitting: under CommandLineActionLauncher the licence is acquired silently, zero dialogs. An interactive com.nomagic.magicdraw.Main start with the identical properties sat on a License Server Connection dialog and never finished loading — the plugin descriptor was parsed and init() never ran. In an unattended run that is indistinguishable from a hang.

⇒ You cannot start an interactive Cameo unattended this way. Get the real server from Creating FlexNet Client: <port>@<host> in %LOCALAPPDATA%\.cameoea\2024x\cea.log. The Siemens licence server on 29000 is a different product and a documented red herring. Interactive starts also raise two further modals (Select Edition, Merge Plugin "not available"), which get misdiagnosed as "won't license on relaunch".

3. A batch process discards an unsaved edit, and the create still says ok

Each one-shot run is its own process. Create an element, return success, exit — and the change never existed. Caught for real: a package created and verifiedByReread: true was simply gone in the next run.

⇒ Save inside the same run, and assert on the artifact:

before = os.path.getmtime(src)
saved  = pm.saveProject(ProjectDescriptorsFactory.getDescriptorForProject(p), True)
after  = os.path.getmtime(src)
persisted = bool(saved) and after > before      # saveProject can return true and change nothing

The session must be closed before the save — a save inside an open edit session is not valid. If a Java wrapper opened the session for you, close it from the script and make the wrapper's own close conditional on isSessionCreated.


The worker: pay startup once

A one-shot batch call costs ~15 s of startup and 35-48 s wall. Register a third action that starts a loopback HTTP server and blocks, and the same work costs 0.4-0.8 s per call (measured: project_info 0.75 s, diagrams 0.44 s, find 0.38 s, against a 51 s one-time start). The project stays open between calls, so element ids stay live and nothing is re-saved and re-opened between two edits.

This is also the only unattended route to the live HTTP surface, precisely because of the GUI licence dialog above.

Build it with com.sun.net.httpserver (in the JDK, no extra jar to conflict on the shared classloader), bound to 127.0.0.1, gated on a per-launch random token written to a handshake file in the config directory. Put parameters in the query string and the script in the raw body — then you need a JSON writer and no parser.

Non-obvious requirements, each of which is a real failure otherwise:

  • Serialize requests. Executors.newSingleThreadExecutor(). The UML model is thread safe for reading only; writing is not.
  • Marshal to the AWT event thread. "The majority of the application code (including OpenAPI) is not thread safe" — Developer Guide, Multi-threading. An HTTP handler thread is not the EDT.
  • Use invokeLater + FutureTask.get(timeout), never invokeAndWait. invokeAndWait cannot be interrupted, so one modal dialog hangs the bridge forever. Time out the wait and return a distinct timeout error kind — the EDT may still be stuck, and saying so is the honest answer.
  • Expire the worker. Idle timeout and total lifetime. It holds a floating licence seat and an open project until it stops.
  • Remove the handshake file on shutdown. A stale one pointing at a dead port makes "not running" look like "running but broken".
  • Refuse /shutdown from an interactive session — that would close an application a person has open.

Session and verification discipline

if not SessionManager.getInstance().isSessionCreated(project):
    SessionManager.getInstance().createSession(project, "My edit")
# ... edits ...
SessionManager.getInstance().closeSession(project)     # cancelSession(project) on error

createSession throws IllegalStateException if one is already open, and only one can be active. Cancel on exception or a raise leaves a dangling session — and verify the cancel with a control: a script that edits the model and then raises should leave zero survivors on a follow-up search. That control passed here; without it, a half-applied edit is indistinguishable from a clean run.

Two verification traps found the same night:

  • Tagged values are coerced to the tag's declared type. Writing "true" reads back "True". Comparing raw strings makes a successful write look like a failure — and since a failure cancels the session, the over-strict check was itself throwing away good writes. Normalise before comparing.
  • Sanitised filenames collide. Exporting diagrams by name reported 8 written with 7 on disk: two diagrams shared a name and one silently overwrote the other. De-duplicate, and report distinct files found on disk next to the count.

Errors that name the wrong thing

Symptom Real cause
Commandline action <FQCN> not found. the plugin did not load. Names the class, never the missing plugin directory. Check -Dmd.plugins.dir, and that plugin.xml + jar are in a <plugins-dir>\<id>\ folder.
error: Invalid filename: <BOM>C:\... from javac a PowerShell 5.1 Set-Content -Encoding utf8 argfile. Use [System.IO.File]::WriteAllLines with UTF8Encoding($false).
Could not find or load main class Enterprise Start-Process -ArgumentList joined an unquoted -D...=D:\Cameo Enterprise Architecture\... on its space. Quote each member, or use an argv list (Python subprocess handles it).
plugin descriptor logged, init() never runs, no error a startup modal is blocking. Check the process's MainWindowTitle.
a run that "worked" but the model is unchanged no save, or a save inside an open session. See §3.

Pure ASCII, when it applies

Jython source that must also run as a registered GUI macro has to be pure ASCII with no encoding declaration: non-ASCII fails under execfile ("Non-ASCII character ... but no encoding declared"), and adding # -*- coding: utf-8 -*- then fails the macro engine with org.python.antlr.ParseException: encoding declaration in Unicode string. No single file satisfies both paths. A script delivered over HTTP or read as UTF-8 by your own action is not subject to this — but keeping to ASCII costs nothing and keeps a script runnable either way.


Generated from skills/cameo-headless-automation/SKILL.md in the tc-automation-skills library, which is the canonical copy and also serves as the agent skill set for Teamcenter work.