Class PluginInstallUtils
- Since:
- 6.0.0
- Author:
- wisdomme
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic final classRaised when the name the operator typed could be more than one loaded module's, so the uninstall changed nothing.private static final classWhat one archive says about which module it holds -- the single reader both identity resolution and entry classification use, so the two cannot drift apart.private static enumWhat one entry of the modules folder is, relative to the module being uninstalled.static final classWhatinstallReporting(String, String)did.private static final classEverything later decisions need about which module this is, resolved once.static final classRaised when a module's own unload threw.private static enumWhat a filesystem probe answered, keeping "unknown" out of "no".private static final classWhat the module loader and the loaded instances say about an uninstall's targets, read before anything is unloaded.static final classThe uninstall could not delete these JARs now -- the running server holds them open, or the folder is not writable -- and has recorded them, so the next start deletes them before any module loads (#518).static final classThe uninstall could not delete these JARs now and could not record their deletion for the next start either (#518).(package private) static final classThe catalogue and downloader of one staging call (plan 22-26 fix round).static final classThe entries an uninstall could not identify, carried on whatever failure leaves the method.static classRaised when the uninstall refuses to act because its outcome would be undefined: two possible targets, or a target whose JAR another running module shares.static final classWhat an uninstall did, and what it could not determine. -
Field Summary
FieldsModifier and TypeFieldDescriptionprivate static String(package private) static final ModuleFileTransactions.CatalogueThe UltiCloud catalogue, as the update transaction asks it.private static String(package private) static final Function<UltiToolsPlugin, File> How the uninstall asks a loaded module which JAR it came from; a test passes its own.private static final Logger(package private) static final StringThe suffix of a download not yet checked; the module loader reads only*.jar. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionabsolutePathsOf(List<File> files) The absolute paths offiles, in the order given.private static voidaddNormalized(Set<String> into, String identifyString) AddsidentifyString, normalised, unless it is absent or blank.private static booleanalreadyAttached(Throwable failure, Throwable candidate) Whethercandidateis already amongfailure's suppressed throwables.private static PluginInstallUtils.EntryStateAn entry whoseplugin.ymldeclares one of the targets' main classes: the module loader loads exactly the classmain:names (#548/#549), so after a restart this file loads the module the operator removed (#516).private static voidcancelUpdatesOf(PluginInstallUtils.ModuleIdentity identity, List<String> cancelledUpdates) Cancels the uninstalled module's staged updates and running update downloads, matched on the identity this uninstall resolved (round-10 review).private static StringcanonicalPathOf(File file) file's canonical path, or its absolute path when it cannot be canonicalised.private static <T extends Throwable>
TAttaches the undetermined entries to a failure on its way out, when there are any.private static voidchooseTargets(String requested, List<UltiToolsPlugin> loaded, List<UltiToolsPlugin> others, Map<UltiToolsPlugin, String> declared) Moves the instances the argument names fromothersintoloaded.private static PluginInstallUtils.EntryStateclassify(File file, PluginInstallUtils.ModuleIdentity identity) WhichPluginInstallUtils.EntryStatean entry of the modules folder is in.(package private) static FilecodeSourceJarOf(Class<?> moduleClass) The JAR a module class was loaded from, ornullwhen it was not loaded from one.(package private) static FilecodeSourceLocationOf(Class<?> moduleClass) Whether which JAR a module was loaded from could be determined at all.private static FileSystemExceptiondeferOrFail(PluginInstallUtils.ModuleIdentity identity, List<File> jars, FileSystemException deleteFailure) A JAR that could not be deleted now is recorded so the next start deletes it before any module loads (#518).private static voiddeleteAllOrThrow(List<File> files) Deletes every file, and reports the ones that are still there.private static PluginInstallUtils.UninstallReportThe JAR half ofuninstallPlugin(String): deletes every entry identified as this module's -- not only the first one listed, since a second JAR loads the module again on restart -- and reports the entries nothing could be read from.private static voiddeleteQuietly(File file) private static PluginInstallUtils.InstallResultdownloadAndAdmit(String url, CatalogueClient.Version metadata, String fileName, String requested, String version, int level) Verifies the download, then checks it as an archive and by its declaredapi-version, and either renames it tofileNameor deletes it.(package private) static voiddownloadVerified(String link, CatalogueClient.Version metadata, String fileName, File folder) The staging download: the same verification the install applies, because a staged update becomes the module that loads at the next start.(package private) static StringfailureLine(String module, String version, VerifiedDownload.Kind kind) The one line an operator sees when a download did not pass its checks: the module, the version and the reason, and that nothing was installed.static FilefindPluginJar(File pluginsFolder, String identifyString) Find the JAR file for a plugin by its identify-string.private static ThrowableThe first failure, with any later one attached to it.private static StringforeignDownloadLine(String module, String version) The line for adownload.urlthe catalogue put outside its own host: refused before anything is requested, because a catalogue answer must never move an executable download.private static voidforgetDeferredRemoval(String fileName) After an install wrotefileNameinto the modules folder, forgets a deletion an earlier uninstall recorded for that name, so the next start does not delete the new install (#518).(package private) static StringGet the base URL for API calls.static PluginEntityGet plugin information by identify string.static StringgetPluginLatestDownloadLink(String idString) Get plugin latest version download link.static StringgetPluginLatestVersion(String idString) Get plugin latest version.static List<PluginEntity> getPluginList(int page, int pageSize) Get plugin list online.static StringgetPluginVersionDownloadLink(String idString, String version) Get plugin download link.getPluginVersions(String idString) Get plugin versions.(package private) static StringinstalledJarName(String idString, String version) static booleaninstallLatestPlugin(String idString) Install latest plugin.static booleaninstallPlugin(String idString, String version) Install plugin.installReporting(String idString, String versionOrNull) Installs a module from the v2 catalogue and says why when it does not (plan 22-18).private static PluginInstallUtils.InstallResultinstallVersion(CatalogueClient.Version metadata, String idString, String requested, String version, int level) The second half ofinstallReporting(java.lang.String, java.lang.String): the version is known.private static booleanprivate static booleanisSafeArchive(File file) private static booleanlicenceRequired(CatalogueClient.Version metadata) static VersionLookuplookUpPluginLatestVersion(String idString) Looks up a module's latest version for this framework's API level and says whether the lookup established anything (Phase 19, D19-032).static VersionLookuplookUpPluginLatestVersion(String idString, BooleanSupplier stopped) lookUpPluginLatestVersion(String), with a stop signal read between its two requests (Phase 19, D19-032): whenstoppedis true once the module is found, thelatestrequest is not sent and the lookup isVersionLookup.Outcome.FAILEDwith the failure"stopped".static VersionLookuplookUpPluginVersions(String idString, List<String> names) Internal command lookup that distinguishes an unknown module from a successful empty list.mainClassesOf(PluginManager pluginManager, List<UltiToolsPlugin> targets, List<UltiToolsPlugin> others) The main classes the module loader recorded for the targets, less every one another loaded module also has (#516) -- the same exclusion the name keys get (gate 1, BL-02).private static FileThe modules folder install and uninstall act on: the onePluginManager#initscans and the module class loader reads, from the one method that computes it (#517).private static AccessDeniedExceptionmodulesFolderUnlistable(File folder) The state-D failure: the folder is there, and what it holds is unknown.private static voidmoveIntoPlace(File part, File target) Renames the checked download to its final name in one step, replacing a file of that name.private static booleannoJarFound(File folder, PluginInstallUtils.ModuleIdentity identity, List<String> undetermined) What "nothing here could be identified as this module's" means, which depends on whether one was loaded.(package private) static StringnormalizeIdentifyString(String idString) private static StringnotAvailableLine(String module, String version) The line for a version the catalogue lists but does not offer for another reason than a licence (that one isPluginInstallUtils.InstallResult.Keys.REASON_LICENCE_REQUIRED); it is not an integrity failure.private static PluginInstallUtils.PresenceWhat is atfile, without following links, distinguishing "not there" from "could not be determined".private static PluginInstallUtils.Presenceprobe(File file, LinkOption... options) probe(File)with the link options to use.private static PluginInstallUtils.PresenceprobeFollowingLinks(File file) What is atfilewith links followed: the question "is this one of the files the module loader would open?", which is a different question from "is anything at this path?".private static PluginInstallUtils.ArchiveIdentityreadArchive(File file) Reads what an archive declares about itself.private static PluginInstallUtils.ArchiveIdentityreadDeclaredName(JarFile jarFile, JarEntry entry, File file) Thenameaplugin.ymldeclares, if it declares one.private static voidreadLoadedModules(PluginManager pluginManager, Function<UltiToolsPlugin, File> codeSource, List<UltiToolsPlugin> others, Map<UltiToolsPlugin, File> jars, Map<UltiToolsPlugin, String> declared, AtomicBoolean someUnknown) Reads every loaded module once: which JAR it came from, and what that JAR declares.(package private) static String(package private) static Function<UltiToolsPlugin, String> recordedMainClass(ModuleJarIndex index) The main class the module loader recorded for a loaded module, read from its start-up index (#516): what staging checks other copies of the module against (maintainer follow-up 19).private static PluginInstallUtils.RecordedrecordedOf(PluginManager pluginManager, List<UltiToolsPlugin> targets, List<UltiToolsPlugin> others, Set<String> ownJars) What the loader recorded about the targets (#516) and what they report about themselves, read before they are unloaded.private static PluginInstallUtils.InstallResultWhy a downloaded JAR is not installed, ornullto install it: the archive guard the module loader applies (SecurityPolicy.isSafeFileStructure(long, int); the.jarsuffix check ofSecurityPolicy.isValidModuleJar(java.io.File)does not apply to the.partname), then the declaredapi-version.private static voidrefuseIfAmbiguous(String requested, List<UltiToolsPlugin> candidates) Refuses when more than one module answers to a declared name.private static voidprivate static voidRefuses, before anything is unloaded, while an update of the module is held for the operator: its record names a file the transaction could not identify, and the uninstall's cancellation would otherwise have to delete that record's folder, files included (foreign-file rule).private static voidrefuseToTouchAnotherModulesJar(PluginInstallUtils.ModuleIdentity identity, List<File> toDelete) The invariant, checked immediately before anything is deleted: no JAR another loaded module was loaded from may be removed, whatever decided to get here.private static PluginInstallUtils.UninstallReportremoveJarsAndReport(String name, PluginInstallUtils.ModuleIdentity identity, Throwable unloadFailure) The JAR half of the uninstall and its report, after the modules were unloaded.(package private) static voidReset the base URL to force re-initialization.private static FileresolveCodeSourceFile(URL location) A code source URL as a file, whatever escaping it carries.private static PluginInstallUtils.ModuleIdentityresolveIdentity(String requested, PluginManager pluginManager, Function<UltiToolsPlugin, File> codeSource) Resolves the operator's argument into the one identity every later decision reads.(package private) static voidSet a custom base URL for testing purposes.stageUpdate(String identifyString) Stages an update of a loaded module to its latest version (#505).static booleanuninstallPlugin(String name) Uninstalls a module: unloads every loaded instance of it and deletes its JARs.uninstallPlugin(String), also reporting the entries whose identity could not be determined -- the ones a caller has to tell the operator about, because one of them may be a copy of this module that loads again at the next start.(package private) static PluginInstallUtils.UninstallReportuninstallPluginReporting(String name, Function<UltiToolsPlugin, File> codeSource) uninstallPluginReporting(String)with the code-source resolver to ask, which is how a test substitutes one without production code carrying a writable field (gate 1, IN-02).(package private) static PluginInstallUtils.UninstallReportuninstallPluginReporting(String name, Function<UltiToolsPlugin, File> codeSource, List<String> cancelledUpdates) uninstallPluginReporting(String, List)with the code-source resolver to ask.uninstallPluginReporting(String name, List<String> cancelledUpdates) uninstallPluginReporting(String), also collecting the versions of the module's updates it cancelled.private static ThrowableunloadEvery(PluginInstallUtils.ModuleIdentity identity, PluginManager pluginManager) Unloads every matching module through the framework's full unload path, which also delists it.private static PluginInstallUtils.ModuleUnloadFailedExceptionunloadFailed(String name, Throwable unloadFailure, IOException jarFailure) The failure raised when a module's own unload threw, after the uninstall went ahead anyway.static booleanupdatePlugin(String identifyString) Stages an update of a loaded module; seestageUpdate(String).
-
Field Details
-
LOGGER
-
DEFAULT_MODULE_CODE_SOURCE
How the uninstall asks a loaded module which JAR it came from; a test passes its own. -
CATALOGUE
The UltiCloud catalogue, as the update transaction asks it. Versions exist but none this framework installs -- the level-filtered list is empty -- is thrown asModuleFileTransactions.NoCompatibleVersionException(plan 18-40). -
baseUrl
-
customBaseUrl
-
PART_SUFFIX
The suffix of a download not yet checked; the module loader reads only*.jar.- See Also:
-
-
Constructor Details
-
PluginInstallUtils
public PluginInstallUtils()
-
-
Method Details
-
getBaseUrl
Get the base URL for API calls. Uses lazy initialization to avoid requiring UltiTools at class loading time.- Returns:
- the base URL
-
setBaseUrlForTesting
Set a custom base URL for testing purposes. This should only be used in unit tests.- Parameters:
url- the custom base URL, or null to reset to default
-
resetBaseUrl
static void resetBaseUrl()Reset the base URL to force re-initialization. This should only be used in unit tests. -
normalizeIdentifyString
-
installedJarName
-
getPluginList
Get plugin list online.- Parameters:
page- the page numberpageSize- the number of items per page- Returns:
- the plugin list
-
getPluginVersionDownloadLink
Get plugin download link.- Parameters:
idString- the plugin IDversion- the version- Returns:
- the plugin download link
-
getPluginVersions
Get plugin versions.- Parameters:
idString- the plugin ID- Returns:
- the plugin version list
-
lookUpPluginVersions
Internal command lookup that distinguishes an unknown module from a successful empty list. This bridge is not part of the supported plugin API;getPluginVersions(String)keeps its existing empty-list/null contract.- Parameters:
idString- the plugin IDnames- collects version names on success- Returns:
FOUNDwith the module identifier on success,NONEwhen the versions endpoint answers 404, orFAILEDwhen the lookup establishes nothing
-
getPluginLatestVersion
Get plugin latest version.Returns
nullfor a blank id, a module the catalogue does not know, no version this framework can install, and every failure (an error status, an unreadable body, an unreachable catalogue); this contract is unchanged as of 6.3.0. The start-up update check useslookUpPluginLatestVersion(String)instead, which tells those apart.- Parameters:
idString- the plugin ID- Returns:
- the plugin's latest version, or
null
-
lookUpPluginLatestVersion
Looks up a module's latest version for this framework's API level and says whether the lookup established anything (Phase 19, D19-032).VersionLookup.Outcome.FOUNDcarries the version exactly asgetPluginLatestVersion(String)returns it.VersionLookup.Outcome.NONEis a confirmed "nothing to offer": a blank id, the catalogue not knowing the module (plugin/getanswers 404), or knowing it with no version this framework can install or no finalized version (latestanswers 409 or 404). Every other answer -- another non-2xx status, an error envelope, a missingdata, an unreadable body, an unreachable catalogue -- isVersionLookup.Outcome.FAILED.An exception thrown while asking propagates, exactly as it does from
getPluginLatestVersion(String).- Parameters:
idString- the module's identify string- Returns:
- the lookup's outcome, never
null - Since:
- 6.3.0
-
lookUpPluginLatestVersion
@Internal public static VersionLookup lookUpPluginLatestVersion(String idString, BooleanSupplier stopped) lookUpPluginLatestVersion(String), with a stop signal read between its two requests (Phase 19, D19-032): whenstoppedis true once the module is found, thelatestrequest is not sent and the lookup isVersionLookup.Outcome.FAILEDwith the failure"stopped". The start-up update check passes its own stop flag, so a check stopped by the plugin's disable sends no further request.- Parameters:
idString- the module's identify stringstopped- read once, after the first request answers- Returns:
- the lookup's outcome, never
null - Since:
- 6.3.0
-
getPluginLatestDownloadLink
Get plugin latest version download link.- Parameters:
idString- Plugin identify string- Returns:
- Download link for latest version
-
getPlugin
Get plugin information by identify string.- Parameters:
idString- Plugin identify string- Returns:
- Plugin entity or null if not found
-
installLatestPlugin
Install latest plugin.As of 6.3.0 only a module whose
plugin.ymldeclares anapi-versionfromModuleApiLevels.MIN_INSTALLABLE_API_LEVELup to this framework's level is installed; for any other this returnsfalseand writes nothing to the modules folder. SeeinstallReporting(String, String).- Parameters:
idString- the plugin ID- Returns:
- whether the install succeeded
-
installPlugin
Install plugin.As of 6.3.0 only a module whose
plugin.ymldeclares anapi-versionfromModuleApiLevels.MIN_INSTALLABLE_API_LEVELup to this framework's level is installed; for any other this returnsfalseand writes nothing to the modules folder. SeeinstallReporting(String, String).- Parameters:
idString- the plugin IDversion- the version- Returns:
- whether the install succeeded
-
installReporting
@Internal public static PluginInstallUtils.InstallResult installReporting(String idString, String versionOrNull) Installs a module from the v2 catalogue and says why when it does not (plan 22-18).The version's metadata names the byte count and the SHA-256 of the JAR it offers, and the JAR is accepted only when both match the bytes that arrived (see
VerifiedDownload). It is downloaded under a name the module loader never lists or puts on the class path (it reads only*.jar), on the same file system so that accepting it is one atomic rename. Itsplugin.ymlis then read again, and the module is refused -- the download deleted, the modules folder as it was -- unless itsapi-versionis an integer fromModuleApiLevels.MIN_INSTALLABLE_API_LEVELto this framework's level. That check is the backstop for a catalogue that does not filter or records a version wrongly. An accepted JAR replaces a file of the same final name, as the download always did.A version the catalogue lists but does not offer for public download (licence required) is refused before anything is requested, with its own line; the framework implements no licensing of its own.
- Parameters:
idString- the module's identify-stringversionOrNull- the version, ornullfor the latest version the catalogue offers this framework- Returns:
- the outcome; a refusal the operator is told about has already been logged
-
installVersion
private static PluginInstallUtils.InstallResult installVersion(CatalogueClient.Version metadata, String idString, String requested, String version, int level) The second half ofinstallReporting(java.lang.String, java.lang.String): the version is known. A version the catalogue does not offer for public download is refused here, before any request, and every other version goes through the verified download. -
downloadAndAdmit
private static PluginInstallUtils.InstallResult downloadAndAdmit(String url, CatalogueClient.Version metadata, String fileName, String requested, String version, int level) Verifies the download, then checks it as an archive and by its declaredapi-version, and either renames it tofileNameor deletes it. No partial file survives this method, whatever happens. -
failureLine
The one line an operator sees when a download did not pass its checks: the module, the version and the reason, and that nothing was installed. A withdrawn version, a transport failure and an integrity failure cannot be mistaken for one another; every integrity failure (hash, size, hash header, no usable hash or size) is the catalogue textPluginInstallUtils.InstallResult.Keys.REASON_INTEGRITY_FAILEDin the server's language (plan 22-22), and which check failed is on the FINE line beside it. Only short hash prefixes may join it in the local log, never a whole URL.- Parameters:
module- the module as the operator asked for itversion- the versionkind- why the download was refused- Returns:
- the line
-
isIntegrityFailure
- Parameters:
kind- why a download was refused- Returns:
- whether the bytes, the header or the catalogue entry failed the integrity check, as opposed to a version that is not offered or a download that did not complete
-
licenceRequired
- Returns:
- whether the catalogue offers the version only with a licence
-
foreignDownloadLine
The line for adownload.urlthe catalogue put outside its own host: refused before anything is requested, because a catalogue answer must never move an executable download. -
notAvailableLine
The line for a version the catalogue lists but does not offer for another reason than a licence (that one isPluginInstallUtils.InstallResult.Keys.REASON_LICENCE_REQUIRED); it is not an integrity failure. -
refusalOf
private static PluginInstallUtils.InstallResult refusalOf(File part, String requested, String version, int level) Why a downloaded JAR is not installed, ornullto install it: the archive guard the module loader applies (SecurityPolicy.isSafeFileStructure(long, int); the.jarsuffix check ofSecurityPolicy.isValidModuleJar(java.io.File)does not apply to the.partname), then the declaredapi-version. -
isSafeArchive
-
moveIntoPlace
Renames the checked download to its final name in one step, replacing a file of that name.- Throws:
IOException
-
deleteQuietly
-
findPluginJar
Find the JAR file for a plugin by its identify-string. Scans all JARs in the given directory, reads plugin.yml from each.- Parameters:
pluginsFolder- the folder to scanidentifyString- the identify-string to match- Returns:
- the matching JAR file, or null if not found
-
stageUpdate
Stages an update of a loaded module to its latest version (#505).Nothing in the modules folder changes: the new JAR is downloaded into the transaction folder beside it and recorded. At the next start the files are swapped before any module loads, and the update is kept only if the module is then seen loaded from the new JAR at the new version; otherwise the old JAR is restored. See
ModuleFileTransactions.- Parameters:
identifyString- the module's identify-string- Returns:
- what was staged, or why nothing was
-
downloadVerified
static void downloadVerified(String link, CatalogueClient.Version metadata, String fileName, File folder) throws IOException The staging download: the same verification the install applies, because a staged update becomes the module that loads at the next start. The link is the version'sdownload.url, used as written; the metadata it is checked against is the version object the caller has just read through the contract's version route, so staging never trusts a hash it did not just fetch.- Parameters:
link- the absolute version download URLmetadata- the version, as the catalogue's version route just answered itfileName- the file name to writefolder- the folder to write it into- Throws:
IOException- when the catalogue offers no public download or the file does not verify
-
reasonOf
- Returns:
- the reason a staged download was refused, as the update reason names it
-
recordedMainClass
The main class the module loader recorded for a loaded module, read from its start-up index (#516): what staging checks other copies of the module against (maintainer follow-up 19).nullfor a module the index has no record of, so that staging falls back to the old JAR's ownplugin.ymlmain:rather than to a runtime class name no JAR declares.- Parameters:
index- the loader's index, ornull- Returns:
- the resolver
-
forgetDeferredRemoval
After an install wrotefileNameinto the modules folder, forgets a deletion an earlier uninstall recorded for that name, so the next start does not delete the new install (#518). A failure is logged: the install itself succeeded. -
updatePlugin
Stages an update of a loaded module; seestageUpdate(String).As of 6.3.0 this no longer replaces the JAR in place and no longer means "updated": it returns
truewhen the update was staged, to take effect at the next start and to be kept only if the module then loads.- Parameters:
identifyString- the plugin identify string- Returns:
trueif the update was staged
-
probe
What is atfile, without following links, distinguishing "not there" from "could not be determined".- Parameters:
file- the path to probe- Returns:
- what is there
-
probeFollowingLinks
What is atfilewith links followed: the question "is this one of the files the module loader would open?", which is a different question from "is anything at this path?".PluginManager#initopens each entry throughnew JarFile(file), which follows links, so a link to a real JAR is a file it loads from. The access question keepsNOFOLLOW_LINKS, because that is what separates absence from a denial; this one must match the loader.- Parameters:
file- the path to probe- Returns:
- what is at the end of it
-
probe
probe(File)with the link options to use.- Parameters:
file- the path to probeoptions- how to treat a symbolic link- Returns:
- what is there
-
codeSourceJarOf
The JAR a module class was loaded from, ornullwhen it was not loaded from one.The loaded instance is the authority on which file it came from, whatever that file's
plugin.ymlsays now: the file may have been replaced or edited since the start, and a module registered from code has noplugin.ymlentry the loader read at all. So the framework asks the instance, exactly asUltiToolsPluginitself does when it reads its own embedded resources.A code source that is a directory is a development checkout rather than an installed JAR and answers nothing about which file to delete. One that cannot be probed is still named: the module was loaded from it, and a probe that cannot answer is not an answer of "no".
- Parameters:
moduleClass- the loaded module's class- Returns:
- the JAR, or
nullwhen there is none to name
-
codeSourceLocationOf
Whether which JAR a module was loaded from could be determined at all.Three answers through one value, because the difference matters: a file is the JAR the module came from; a directory is an exploded development checkout, which is no JAR at all and says so; and
nullis "could not be determined" -- a code source a policy refuses to reveal, or a class that has none. Unknown is not "none": while it holds, any entry matched only by a name might be that running module's own JAR.- Parameters:
moduleClass- the loaded module's class- Returns:
- the code source's location, or
nullwhen it could not be determined
-
resolveCodeSourceFile
A code source URL as a file, whatever escaping it carries. -
readArchive
Reads what an archive declares about itself.The one place a
plugin.ymlis opened. Four outcomes, each a positive answer: it declares a name; it opened and declares none (noplugin.yml, or one without aname:key -- which say the same thing, since a module needs no metadata at all); it could not be opened or parsed; or the entry is one the module loader itself would never load.- Parameters:
file- the entry- Returns:
- what it says about itself
-
readDeclaredName
private static PluginInstallUtils.ArchiveIdentity readDeclaredName(JarFile jarFile, JarEntry entry, File file) throws IOException Thenameaplugin.ymldeclares, if it declares one.- Parameters:
jarFile- the archiveentry- itsplugin.ymlfile- the archive's path, for the log line- Returns:
- what it says about itself
- Throws:
IOException- when the entry cannot be read
-
canonicalPathOf
file's canonical path, or its absolute path when it cannot be canonicalised. -
carrying
Attaches the undetermined entries to a failure on its way out, when there are any.- Type Parameters:
T- the failure's type- Parameters:
failure- the failure leaving the uninstallentries- the absolute paths of the entries nothing could identify- Returns:
failure
-
uninstallPlugin
Uninstalls a module: unloads every loaded instance of it and deletes its JARs.Unloading goes through
PluginManager.unregister(UltiToolsPlugin), the framework's one full unload path (#503). Callingplugin.unregisterSelf()directly, as this method used to, skipped everythingunregisterdoes first -- cancelling the module's@Scheduledtasks, releasing its@PlayerCachebeans, its tab-completion completers, its EventBus handlers and its conditional-bean records, then closing its context. A module "uninstalled" that way kept running its repeating tasks until the next restart.What it reports is what happened on disk (#501). The old implementation ignored
File.delete()'s result and returnedtruewhether or not the JAR was removed, and it stopped at the first matching JAR, so a second JAR of the same module loaded it again on restart. Now every entry the modules folder holds is placed in exactly onePluginInstallUtils.EntryState, and what the method returns or throws says which of those it met.As of 6.3.0 an entry is this module's also when its
plugin.ymlmain:names the module's main class, the entry the module loader loads by (#516), and an uninstall that goes ahead also cancels any update of the module waiting for the next start (uninstallPluginReporting(String, List)).- Parameters:
name- the module's runtime name -- which need not be what its JAR'splugin.ymldeclares, sinceUltiToolsPlugin(String pluginName, ...)takes it as an argument and reads no metadata- Returns:
truewhen every entry identified as this module's was deleted;falsewhen none was and no loaded module of that name was unloaded either -- the "check the spelling" case- Throws:
AccessDeniedException- when the modules folder exists but could not be listed, so nothing can be concluded about what it holdsFileSystemException- when an entry identified as this module's could not be deleted, naming every one still on disk: everyFileSystemExceptionin the failure, including those attached as suppressed, names a module JAR still on disk. When the deletion could not be recorded for the next start either, that failure travels attached as aPluginInstallUtils.RemovalNotRecordedException, which is not aFileSystemExceptionNoSuchFileException- when a loaded module was unloaded but nothing in the folder could be identified as its JAR, which is not a spelling mistakeIllegalStateException- when the module's own unload threw. The module is still removed from the loaded modules and its JARs are still deleted:unregisterhas closed its context by then, and keeping the JAR would bring back on restart a module the operator asked to remove. A JAR failure is attached as suppressed.IOException- if the modules folder cannot be read
-
uninstallPluginReporting
@Internal public static PluginInstallUtils.UninstallReport uninstallPluginReporting(String name) throws IOException uninstallPlugin(String), also reporting the entries whose identity could not be determined -- the ones a caller has to tell the operator about, because one of them may be a copy of this module that loads again at the next start.- Parameters:
name- the module's runtime name- Returns:
- what the uninstall did and what it could not determine
- Throws:
IOException- exactly asuninstallPlugin(String)documents
-
uninstallPluginReporting
@Internal public static PluginInstallUtils.UninstallReport uninstallPluginReporting(String name, List<String> cancelledUpdates) throws IOException uninstallPluginReporting(String), also collecting the versions of the module's updates it cancelled.An uninstall that goes ahead -- whatever happens to its JARs afterwards, including a modules folder that cannot be listed -- cancels every update of the module still waiting for the next start and any download of one still running, so the next start does not install the module again (#505). It matches them on the identity it resolved before touching the folder: the identify-strings and runtime names of the instances it unloaded and their JARs, never only the name it was given (round-10 review). A refused uninstall changed nothing and cancels nothing.
- Parameters:
name- the module's runtime name, or a name a JAR of it declarescancelledUpdates- collects the version of each staged update cancelled; filled on every outcome, including a thrown one, except a refusal- Returns:
- what the uninstall did and what it could not determine
- Throws:
IOException- exactly asuninstallPlugin(String)documents
-
uninstallPluginReporting
@Internal static PluginInstallUtils.UninstallReport uninstallPluginReporting(String name, Function<UltiToolsPlugin, File> codeSource) throws IOExceptionuninstallPluginReporting(String)with the code-source resolver to ask, which is how a test substitutes one without production code carrying a writable field (gate 1, IN-02).- Parameters:
name- the module's runtime name, or a name a JAR of it declarescodeSource- how to ask a loaded module which JAR it came from- Returns:
- what the uninstall did and what it could not determine
- Throws:
IOException- exactly asuninstallPlugin(String)documents
-
uninstallPluginReporting
static PluginInstallUtils.UninstallReport uninstallPluginReporting(String name, Function<UltiToolsPlugin, File> codeSource, List<String> cancelledUpdates) throws IOExceptionuninstallPluginReporting(String, List)with the code-source resolver to ask.- Parameters:
name- the module's runtime name, or a name a JAR of it declarescodeSource- how to ask a loaded module which JAR it came fromcancelledUpdates- collects the version of each staged update cancelled- Returns:
- what the uninstall did and what it could not determine
- Throws:
IOException- exactly asuninstallPlugin(String)documents
-
removeJarsAndReport
private static PluginInstallUtils.UninstallReport removeJarsAndReport(String name, PluginInstallUtils.ModuleIdentity identity, Throwable unloadFailure) throws IOException The JAR half of the uninstall and its report, after the modules were unloaded.- Parameters:
name- the operator's argumentidentity- the resolved identityunloadFailure- what unloading threw, ornull- Returns:
- the report
- Throws:
IOException- as documented onuninstallPlugin(String)
-
cancelUpdatesOf
private static void cancelUpdatesOf(PluginInstallUtils.ModuleIdentity identity, List<String> cancelledUpdates) Cancels the uninstalled module's staged updates and running update downloads, matched on the identity this uninstall resolved (round-10 review). Never throws: it runs on every exit of the uninstall, and a failure here must not replace the outcome being reported.- Parameters:
identity- the resolved identitycancelledUpdates- collects the versions cancelled
-
chooseTargets
private static void chooseTargets(String requested, List<UltiToolsPlugin> loaded, List<UltiToolsPlugin> others, Map<UltiToolsPlugin, String> declared) Moves the instances the argument names fromothersintoloaded.The two namespaces are asked in order, never together. A runtime-name match answers the question outright: if any loaded module answers to the name that was typed, that is the answer and no declared-name lookup runs beside it -- asking both at once made one module's runtime name and another's declared name equally good, and unloaded both.
Only when nothing answers by runtime name is the other namespace consulted, because naming a module by what its JAR declares must still work (gate 1, BL-01). If more than one module answers that way, the uninstall refuses and names the candidates rather than choosing: this deletes files, and a destructive command with two possible targets must stop.
- Parameters:
requested- the operator's argumentloaded- collects the instances it namesothers- every loaded instance; the chosen ones are moved out of itdeclared- what each instance's JAR declares
-
refuseIfAmbiguous
Refuses when more than one module answers to a declared name.- Parameters:
requested- the operator's argumentcandidates- the loaded modules whose JARs declare it
-
resolveIdentity
private static PluginInstallUtils.ModuleIdentity resolveIdentity(String requested, PluginManager pluginManager, Function<UltiToolsPlugin, File> codeSource) Resolves the operator's argument into the one identity every later decision reads.Argument to loaded instances: an instance matches when its runtime name is the argument, or when the JAR it was loaded from declares that name -- the second half is gate 1's BL-01, where naming a module by what its JAR declares deleted the JAR of a module that was left running.
Instances to keys: what their own JARs declare, plus the argument. A key another loaded module owns -- its runtime name, or what its own JAR declares -- is dropped, which is gate 1's BL-02: a name lifted off one module's JAR is not safe as a global key. Those modules' JARs are also remembered, because no decision may end in deleting one of them.
With nothing loaded, the argument is the only key there is, and that is recorded.
- Parameters:
requested- the operator's argumentpluginManager- the manager that owns the plugin listcodeSource- how to ask a loaded module which JAR it came from- Returns:
- the resolved identity
-
recordedOf
private static PluginInstallUtils.Recorded recordedOf(PluginManager pluginManager, List<UltiToolsPlugin> targets, List<UltiToolsPlugin> others, Set<String> ownJars) What the loader recorded about the targets (#516) and what they report about themselves, read before they are unloaded.- Parameters:
pluginManager- the manager whose start-up scan recorded themtargets- the loaded instances this uninstall targetsothers- every other loaded instanceownJars- the canonical paths of the targets' code-source JARs- Returns:
- the record
-
addNormalized
AddsidentifyString, normalised, unless it is absent or blank. -
mainClassesOf
private static Set<String> mainClassesOf(PluginManager pluginManager, List<UltiToolsPlugin> targets, List<UltiToolsPlugin> others) The main classes the module loader recorded for the targets, less every one another loaded module also has (#516) -- the same exclusion the name keys get (gate 1, BL-02).- Parameters:
pluginManager- the manager whose start-up scan recorded themtargets- the loaded instances this uninstall targetsothers- every other loaded instance- Returns:
- the main classes an entry is matched on
-
readLoadedModules
private static void readLoadedModules(PluginManager pluginManager, Function<UltiToolsPlugin, File> codeSource, List<UltiToolsPlugin> others, Map<UltiToolsPlugin, File> jars, Map<UltiToolsPlugin, String> declared, AtomicBoolean someUnknown) Reads every loaded module once: which JAR it came from, and what that JAR declares.Read before anything is unloaded: afterwards the instance and its class loader may be gone, and with them the only authoritative answer to "which JAR is this module's". Which of them the argument names is decided afterwards, by
chooseTargets(java.lang.String, java.util.List<com.ultikits.ultitools.abstracts.UltiToolsPlugin>, java.util.List<com.ultikits.ultitools.abstracts.UltiToolsPlugin>, java.util.Map<com.ultikits.ultitools.abstracts.UltiToolsPlugin, java.lang.String>).- Parameters:
pluginManager- the manager that owns the plugin listcodeSource- how to ask a loaded module which JAR it came fromothers- collects every loaded instance, before any is chosen as a targetjars- collects each instance's code-source JARdeclared- collects what each instance's JAR declaressomeUnknown- set when some instance's code source could not be determined at all
-
unloadEvery
private static Throwable unloadEvery(PluginInstallUtils.ModuleIdentity identity, PluginManager pluginManager) Unloads every matching module through the framework's full unload path, which also delists it.A module that throws while unloading is collected rather than propagated: it is already unloaded and its context already closed, so the rest of the uninstall goes ahead and the failure is reported together with the JAR outcome. It leaves the plugin list either way, because a listed module whose context is closed would be reported as still loaded.
- Parameters:
identity- the resolved identity, whose loaded instances are unloadedpluginManager- the manager that owns the plugin list- Returns:
- the first unload failure with any later one attached, or
null
-
firstOf
The first failure, with any later one attached to it.Combining two failures has more ways to fail than it looks.
addSuppressedrejects a throwable suppressing itself, which a module reusing a static sentinel produces from two instances -- and thatIllegalArgumentExceptionwould escape past every handler, abandoning the uninstall with the modules delisted and their JARs still on disk. It also rejectsnull. Attaching the same object twice is not an error but says nothing, so it is skipped too.- Parameters:
first- the failure collected so far, ornullnext- the failure to attach- Returns:
- the failure to carry on with
-
alreadyAttached
Whethercandidateis already amongfailure's suppressed throwables. -
deleteModuleJars
private static PluginInstallUtils.UninstallReport deleteModuleJars(PluginInstallUtils.ModuleIdentity identity) throws IOException The JAR half ofuninstallPlugin(String): deletes every entry identified as this module's -- not only the first one listed, since a second JAR loads the module again on restart -- and reports the entries nothing could be read from.- Parameters:
identity- the resolved identity: which instances were loaded, which JARs they came from, which keys other entries are matched against, and which JARs belong to other loaded modules- Returns:
- what was deleted and what could not be identified
- Throws:
IOException- as documented onuninstallPlugin(String)
-
deferOrFail
private static FileSystemException deferOrFail(PluginInstallUtils.ModuleIdentity identity, List<File> jars, FileSystemException deleteFailure) A JAR that could not be deleted now is recorded so the next start deletes it before any module loads (#518). On Windows the shared module class loader keeps every module JAR open while the server runs, so "delete it by hand" is an instruction the operator cannot follow; the record is what makes the uninstall complete. Only when the record itself cannot be written does the original failure leave as it was, with the record failure attached inside aPluginInstallUtils.RemovalNotRecordedException: everyFileSystemExceptionin the failure still names a module JAR still on disk, never a record or temporary file under.ultikits.- Parameters:
identity- the resolved identityjars- the JARs the uninstall tried to deletedeleteFailure- what the deletion threw- Returns:
- the failure to report:
PluginInstallUtils.RemovalDeferredExceptionwhen the deletion was recorded; otherwisedeleteFailure, with aPluginInstallUtils.RemovalNotRecordedExceptionattached when the record could not be written
-
refuseIfAnUpdateIsHeldForTheOperator
private static void refuseIfAnUpdateIsHeldForTheOperator(PluginInstallUtils.ModuleIdentity identity) Refuses, before anything is unloaded, while an update of the module is held for the operator: its record names a file the transaction could not identify, and the uninstall's cancellation would otherwise have to delete that record's folder, files included (foreign-file rule). -
refuseToTouchAnotherModulesJar
private static void refuseToTouchAnotherModulesJar(PluginInstallUtils.ModuleIdentity identity, List<File> toDelete) The invariant, checked immediately before anything is deleted: no JAR another loaded module was loaded from may be removed, whatever decided to get here.Gate 1's BL-02 was a bystander module losing its JAR because a name lifted off the target's JAR became the match key. Resolution no longer produces such a key, and this is the guard at the point of destruction for every other way it could happen. It fires before the first delete, so nothing is half-done when it does.
- Parameters:
identity- the resolved identity, carrying the other loaded modules' JARstoDelete- the entries about to be deleted
-
modulesFolder
The modules folder install and uninstall act on: the onePluginManager#initscans and the module class loader reads, from the one method that computes it (#517). Before 6.3.0 the scan readSystem.getProperty("user.dir")+/plugins/UltiTools/pluginswhile this read the data folder, and the two diverged on a server started from another directory.- Returns:
- the modules folder
-
modulesFolderUnlistable
The state-D failure: the folder is there, and what it holds is unknown. -
classify
private static PluginInstallUtils.EntryState classify(File file, PluginInstallUtils.ModuleIdentity identity) WhichPluginInstallUtils.EntryStatean entry of the modules folder is in.The loaded instance's own code-source JAR is state A whatever it declares -- the module itself said where it came from. Otherwise the archive is read once (
readArchive(java.io.File)): an entry the module loader would never load, and one declaring a module that is not this one, are state B; one that declares a key this identity owns is state A; and one that says nothing about itself -- would not open, cannot be parsed, carries noplugin.ymlor noname:-- is state C.- Parameters:
file- the entryidentity- the resolved identity, whose keys and own JARs decide this- Returns:
- the state it is in
-
byDeclaredMainClass
private static PluginInstallUtils.EntryState byDeclaredMainClass(PluginInstallUtils.ModuleIdentity identity) An entry whoseplugin.ymldeclares one of the targets' main classes: the module loader loads exactly the classmain:names (#548/#549), so after a restart this file loads the module the operator removed (#516). The same caution as a name match applies: while some loaded module's own JAR could not be determined, the entry may be that module's, and is reported.- Parameters:
identity- the resolved identity- Returns:
- the entry's state
-
absolutePathsOf
The absolute paths offiles, in the order given. -
deleteAllOrThrow
Deletes every file, and reports the ones that are still there.- Parameters:
files- the JARs to delete- Throws:
FileSystemException- naming the first JAR that survived, with any other attached as suppressed -- each one of them loads the module again on restart
-
noJarFound
private static boolean noJarFound(File folder, PluginInstallUtils.ModuleIdentity identity, List<String> undetermined) throws NoSuchFileException What "nothing here could be identified as this module's" means, which depends on whether one was loaded.- Parameters:
folder- the modules folderidentity- the resolved identityundetermined- the entries nothing could identify, attached to the failure- Returns:
false, the "no such module" answer, when nothing was unloaded either- Throws:
NoSuchFileException- when a module was unloaded and its JAR is missing, with one suppressed entry per undetermined file
-
unloadFailed
private static PluginInstallUtils.ModuleUnloadFailedException unloadFailed(String name, Throwable unloadFailure, IOException jarFailure) The failure raised when a module's own unload threw, after the uninstall went ahead anyway.- Parameters:
name- the module's runtime nameunloadFailure- what its unload threwjarFailure- the JAR outcome to attach, ornullwhen the JARs were deleted- Returns:
- the exception to throw
-