wiki:KrakenCore

Kraken Core

Introduction

Kraken Core is an Apache Felix based OSGi application platform. You can dynamically install or uninstall OSGi bundles at runtime. If you are a Java developer then it will be easy to understand, because eclipse's plugin system is based on OSGi technology. Eclipse uses Equinox OSGi framework.

Kraken Core is packaged into a single JAR file (only 3MB), so you can copy and run OSGi container anywhere with one-line command: java -jar kraken-core-VERSION-package.jar Of course, you may need some other JVM specific options (max/min memory usage, garbage collection strategy, and so on)

You can run krakenapps modules in other OSGi container or use it as a library, but you cannot run kraken script in that case. Kraken Core provides very convenient telnet and ssh shell. Kraken shell support auto-completion via the tab key. ANSI escape code and/or precise telnet option control (e.g. echo off for password input) are also supported. You can read manual anytime if you pass invalid command arguments. But most important thing is that you can add custom commands very easily.

Installation

Latest development (trunk) version is  kraken-core-1.9.2-package.jar. Download kraken-core-VERSION-package.jar file, and run it. JRE 6 is recommended.

$ java -Xmx500M -jar kraken-core-1.9.2-package.jar
[2010-10-27 04:03:32,543]  INFO (Kraken) - Default logging enabled. Configure log4j.properties file for custom logging.
[2010-10-27 04:03:32,859]  INFO (Kraken) - Booting Kraken.
[2010-10-27 04:03:33,112]  INFO (Kraken) - Console localhost/127.0.0.1:7004 opened.
[2010-10-27 04:03:33,385]  INFO (SecurityUtils) - Trying to register BouncyCastle as a JCE provider
[2010-10-27 04:03:34,110]  INFO (SecurityUtils) - Registration succeeded
[2010-10-27 04:03:34,259]  INFO (Kraken) - Kraken started.
[2010-10-27 04:03:34,263]  INFO (BundleManager) - Starting org.apache.felix.framework [0] bundle.

-Xmx500M means that JVM can allocate maximum 500MB of memory. If you have sufficient memory, you can reserve more memory space. There are also other JVM options, for example, -Xms (minimum memory), and so on.

Download and run, That's all.

Basic Commands

Connect to kraken shell using telnet or ssh:

telnet localhost 7004
..or..
ssh localhost -p7022

Default SSH account/password is root/kraken. Telnet port is bound to localhost, so you cannot connect telnet server from remote machine. SSH don't have this limitation. Telnet connection does not require authentication, however it will be deprecated soon.

Anyway, welcome to kraken world!

Shutdown

Type shutdown at kraken shell:

kraken> shutdown
..or..
kraken> bundle.stop 0

bundle.stop 0 command will stop the system bundle, and it has exactly same effect.

If you use Oracle Sun JVM and Unix environment, you can shutdown kraken core gracefully using signal:

kill -SIGTERM [PID]

Account

List current accounts:

kraken> account.list
Accounts
==========
root

Create new account:

kraken> account.create
Description

        create the account

Arguments

        1. name: name of the account (required)
kraken> account.create xeraph
password: 
confirm password: 
kraken> account.list
Accounts
==========
root
xeraph

Change password:

kraken> account.passwd xeraph
current password: 
new password: 
confirm password: 
password changed successfully
kraken>

Delete account:

kraken> account.remove
Description

        remove the account

Arguments

        1. name: name of the account (required)
kraken> account.remove xeraph

Bundle

Maybe you already know about JAR file. Bundle is a JAR that contains also OSGi metadata in manifest. If you want to convert a library to bundle, see  bnd pages. Kraken Core is an OSGi container ( Apache Felix based), so it can support dynamic bundle installation.

List all bundles:

kraken> bundle.list
..or..
kraken> bundle.list [filter text]

At first time, there is only system bundle like this:

kraken> bundle.list
[ ID] Symbolic Name                             Version   Status
==================================================================
[  0] org.apache.felix.framework                2.0.5     ACTIVE

List all bundle repositories:

kraken> bundle.repositories
Maven Bundle Repository
======================================================================
[(  1) krakenapps] http://download.krakenapps.org/
[(  0) maven] http://repo1.maven.org/maven2/

Add new bundle repository configuration:

kraken> bundle.addRepository [repo alias] [url]

Delete bundle repository configuration:

kraken> bundle.removeRepository [repo alias]

Install bundle:

kraken> bundle.install [group id] [artifact id] [version]

For example:

kraken> bundle.install org.apache.felix org.apache.felix.ipojo 1.4.0
Resolving org.apache.felix/org.apache.felix.ipojo (1.4.0)
  -> trying to download from http://download.krakenapps.org/
bundle [1] loaded
kraken> bundle.list
[ ID] Symbolic Name                             Version   Status
==================================================================
[  0] org.apache.felix.framework                2.0.5     ACTIVE
[  1] org.apache.felix.ipojo                    1.4.0     INSTALLED

"bundle.install [group id] [artifact id] [version]" command finds bundles from bundle repositories, download it, and install it. But you may want to install from file system. At that time, type like this:

kraken> bundle.install file://[path]

For example:

In unix: kraken> bundle.install file:///root/kraken/kraken-ipojo-1.1.0.jar
In windows: kraken> bundle.install file:///d:/kraken/kraken-ipojo/target/kraken-ipojo-1.1.0.jar

Note that one MORE slash needed in windows environment.

Uninstall bundle:

kraken> bundle.uninstall [bundle id]

You can uninstall multiple bundles at once, for example:

kraken> bundle.uninstall 2 3 4 

Package

List all current installed packages:

kraken> pkg.list

List all current package repositories:

kraken> pkg.repositories
Kraken Package Repository
=========================
[krakenapps] http://krakenapps.org/mvn/kraken/

You can see default krakenapps package repository. You can also your own package repository. It will be explained as an advanced topic.

Add new kraken package repository configuration:

kraken> pkg.addRepository [repo alias] [url]

For example (default config again):

kraken> pkg.addRepository krakenapps http://krakenapps.org/mvn/kraken/

Delete package repository configuration:

kraken> pkg.removeRepository [repo alias]

For example (if you want to delete default package repository):

pkg.removeRepository krakenapps

Logging

Kraken Core writes all logs to log/ directory, and retains only 1 week logs by default. LogCleaner purges old log/kraken.log.yyyy-MM-dd files. You can find "Kraken Log Cleaner" thread using thread.list script. Logging can be done by using OSGi Log Service or SLF4J alternatively. I prefer SLF4J. :)

List all loggers:

kraken> logger.list [filter text]

For example:
kraken> logger.list
Logger List
----------------
org.apache.felix.framework.Felix
org.krakenapps.account.AccountManagerImpl
org.krakenapps.bundle.BundleManager
org.krakenapps.console.ScriptRunner
org.krakenapps.console.ShellSession
org.krakenapps.console.TelnetHandler
org.krakenapps.console.TelnetStateMachine
org.krakenapps.jpa.impl.HibernateJpaService
org.krakenapps.jpa.impl.JpaConfig
..omitted..

kraken> logger.list jpa
Logger List
----------------
org.krakenapps.jpa.impl.HibernateJpaService
org.krakenapps.jpa.impl.JpaConfig

In general, Java developers use class name as a logger name. In other words, they create logger instance like this:

final Logger logger = LoggerFactory.getLogger(RpcAgentImpl.class.getName());

Switch Debug/Trace Logger:

Turn on debug/trace log:
kraken> logger.on [logger name]

Turn off debug/trace log:
kraken> logger.off [logger name]

Log tracing in realtime:

kraken> logger.tail
(press ctrl-c to stop)

Log level control:

kraken> logger.set [logger name] [log level] [on/off]

For example:
kraken> logger.set org.krakenapps.jpa.impl.HibernateJpaService trace on
kraken> logger.set org.krakenapps.jpa.impl.HibernateJpaService info off

OSGi Service Monitoring

List all OSGi services:

kraken> osgi.services
========= OSGi Services =========
[  1] org.apache.felix.framework.StartLevelImpl
[  2] org.apache.felix.framework.PackageAdminImpl
[  3] org.krakenapps.logger.KrakenLogService
[  4] org.apache.felix.prefs.impl.PreferencesServiceImpl
...omitted...

Inspect properties of an OSGi service:

kraken> osgi.properties [service.id]

For example:

kraken> osgi.properties 20
====== OSGi Service Properties ======
alias: sunperf
objectClass: [Ljava.lang.String;@71dc3d
service.id: 20

Thread Monitoring and Control

List all threads:

kraken> thread.list
[  2] Reference Handler, Group: system, State: WAITING, Priority: 10
[  3] Finalizer, Group: system, State: WAITING, Priority: 8
[  4] Signal Dispatcher, Group: system, State: RUNNABLE, Priority: 9
[  5] Attach Listener, Group: system, State: RUNNABLE, Priority: 5
[  8] Kraken Log Cleaner, Group: main, State: TIMED_WAITING, Priority: 5
[ 10] FelixDispatchQueue, Group: main, State: WAITING, Priority: 5
[ 11] FelixStartLevel, Group: main, State: WAITING, Priority: 5
[ 12] FelixPackageAdmin, Group: main, State: WAITING, Priority: 5
[ 13] Thread-1, Group: main, State: TIMED_WAITING, Priority: 5
[ 14] NioSocketAcceptor-1, Group: main, State: RUNNABLE, Priority: 5
[ 15] NioSocketAcceptor-2, Group: main, State: RUNNABLE, Priority: 5
[ 16] DestroyJavaVM, Group: main, State: RUNNABLE, Priority: 5
[ 17] NioProcessor-2, Group: main, State: RUNNABLE, Priority: 5
[ 31] Thread-15, Group: main, State: RUNNABLE, Priority: 5

You can see all threads actively. There is log cleaner named 'Kraken Log Cleaner' for periodic log purging.

Check thread's stackframe:

kraken> thread.stacks [thread id]
..or..
kraken> thread.stacks [filter text for class name or methods]

For example:

kraken> thread.stacks 8
ID: 8, Name: Kraken Log Cleaner, State: TIMED_WAITING
        java.lang.Thread.sleep
        org.krakenapps.logger.LogCleaner.run (LogCleaner.java:58)
        java.lang.Thread.run

Interrupt thread:

kraken> thread.interrupt [thread id]

Sometimes, buggy multithread logic raises deadlock condition or awaits infinitely. You can manually interrupt that kinds of threads. If you interrupt 'kraken Log Cleaner', it will wake up and work immediately.

Stop the thread:

kraken> thread.stop [thread id]

In production environment, you can stop malfunction thread using this command. However, it should be very careful, and not recommended.

Performance Monitoring

Force Garbage Collection:

kraken> perf.gc
gc called
pending object finalization count: 0

JVM memory monitoring:

kraken> perf.memory
Free memory: 183,225,840 bytes
Total memory: 755,433,472 bytes
Maximum memory: 755,433,472 bytes

Count processors:

kraken> perf.processors
Processors: 2

System information:

kraken> perf.system
Architecture: i386
Operating system: Linux 2.6.27-11-server
Load average: 0.8
System Uptime: 52155721 ms

System Resource Monitoring:

Oracle Sun JVM supports native system resource monitoring:

kraken> sunperf.system
Process cpu time: 31,484,000,000,000
Free phys memory: 51,187,712/2,076,430,336
Free swap space: 3,682,578,432/3,997,442,048
Commited virtual memory: 1,116,377,088
open fd: 448/63536

Thread CPU usage monitoring:

Oracle Sun JVM supports thread cpu usage monitoring:

kraken> sunperf.topThreads
Thread CPU Usages
--------------------
TID 166: 10000000
        java.net.PlainDatagramSocketImpl.receive0 (PlainDatagramSocketImpl.java)
        java.net.PlainDatagramSocketImpl.receive (PlainDatagramSocketImpl.java:136)
        java.net.DatagramSocket.receive (DatagramSocket.java:725)
        org.snmp4j.transport.DefaultUdpTransportMapping$ListenThread.run
        java.lang.Thread.run (Thread.java:619)
        org.snmp4j.util.DefaultThreadFactory$WorkerThread.run
TID 76: 10000000
        java.lang.Object.wait (Object.java)
        java.lang.Object.wait (Object.java:485)
        org.snmp4j.Snmp.send
        org.snmp4j.Snmp.send
        org.snmp4j.Snmp.getBulk
        com.nchovy.baleen.monitor.snmp.AgentLogger.query (AgentLogger.java:427)
        com.nchovy.baleen.monitor.snmp.AgentLogger.runOnce (AgentLogger.java:100)
        org.krakenapps.log.api.AbstractLogger.run (AbstractLogger.java:207)
        java.lang.Thread.run (Thread.java:619)

Idle threads are not listed, and sorted by cpu usage. This command is supported since kraken core 1.7.0.

Keystore Management

Kraken API exposes KeyStoreManager interface, and Kraken Core provides KeyStoreManager OSGi service. User can register arbitrary keystore file, and any OSGi services can get KeyStoreManager service reference and use it. In other words, keystore management is centralized. Kraken Package Manager also use KeyStoreManager service for HTTPS repository access.

KeyStoreManager interface:

public interface KeyStoreManager {
	/**
	 * Return all key store names
	 */
	Collection<String> getKeyStoreNames();
	
	/**
	 *  Return all properties of the key store.
	 */
	Properties getKeyStoreProperties(String alias);
	
	/**
	 * Find and return key store.
	 */
	KeyStore getKeyStore(String name);

	/**
	 * Register file keystore. it is preserved permanently.
	 */
	void registerKeyStore(String name, String type, File file, char[] password) throws KeyStoreException,
			FileNotFoundException, NoSuchAlgorithmException, CertificateException, IOException;

	/**
	 * Register generic keystore. it is not preserved when kraken core reboots.
	 */
	void registerKeyStore(String name, String type, InputStream is, char[] password) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException;

	/**
	 * Unregister keystore.
	 */
	void unregisterKeyStore(String name);

	/**
	 * Return new key manager factory using specified alias.
	 */
	KeyManagerFactory getKeyManagerFactory(String name, String algorithm) throws NoSuchAlgorithmException,
			UnrecoverableKeyException, KeyStoreException;

	/**
	 * Return new trust manager factory using specified alias.
	 */
	TrustManagerFactory getTrustManagerFactory(String name, String algorithm) throws KeyStoreException,
			NoSuchAlgorithmException;
}

List all keystores:

kraken> keystore.list
Key Stores
=============

At first, there is no keystores.

Add new keystore config:

kraken> keystore.register test JKS d:/certs/server.jks PASSWORD
[test] key store registered
kraken> keystore.list
Key Stores
=============
[test] type: JKS, password: PASSWORD, path: d:\certs\server.jks

Delete keystore config:

kraken> keystore.unregister [name]

For example:
kraken> keystore.unregister test

List all key aliases in the keystore:

kraken> keystore.aliases test
Aliases
=============
server, cert=false, key=true

Print certificate details in the keystore:

kraken> keystore.cert test server
[
[
  Version: V3
  Subject: CN=IM00000001, OU=RND, O=NCHOVY, L=Guro, ST=Seoul, C=KR
  Signature Algorithm: SHA1withDSA, OID = 1.2.840.10040.4.3

  Key:  Sun DSA Public Key
    Parameters:DSA
        p:     fd7f5381 1d751229 52df4a9c 2eece4e7 f611b752 3cef4400 c31e3f80 b6512669
    455d4022 51fb593d 8d58fabf c5f5ba30 f6cb9b55 6cd7813b 801d346f f26660b7
    6b9950a5 a49f9fe8 047b1022 c24fbba9 d7feb7c6 1bf83b57 e7c6a8a6 150f04fb
    83f6d3c5 1ec30235 54135a16 9132f675 f3ae2b61 d72aeff2 2203199d d14801c7
        q:     9760508f 15230bcc b292b982 a2eb840b f0581cf5
        g:     f7e1a085 d69b3dde cbbcab5c 36b857b9 7994afbb fa3aea82 f9574c0b 3d078267
    5159578e bad4594f e6710710 8180b449 167123e8 4c281613 b7cf0932 8cc8a6e1
    3c167a8b 547c8d28 e0a3ae1e 2bb3a675 916ea37f 0bfa2135 62f1fb62 7a01243b
    cca4f1be a8519089 a883dfe1 5ae59f06 928b665e 807b5525 64014c3b fecf492a

  y:
    42092f7a e375a3b6 538db1dc cae33cd1 18f34bb4 92651f47 559428ff 37650594
    21795363 cb720b66 b5fdfd08 9d5b99b8 dd1bcd59 89ef5843 8db4123d 11582d68
    b359e4f8 a0295d6f 20049c13 e96f4205 40dde85c d2c11673 c4c55066 c082ce0d
    5a401aa2 1ad26edf a00cc882 49a61912 0338a1a4 bf46f9b8 468157ef befc3589

  Validity: [From: Fri May 21 14:20:59 KST 2010,
               To: Thu Aug 19 14:20:59 KST 2010]
  Issuer: CN=IM00000001, OU=RND, O=NCHOVY, L=Guro, ST=Seoul, C=KR
  SerialNumber: [    4bf6183b]

]
  Algorithm: [SHA1withDSA]
  Signature:
0000: 30 2C 02 14 2D FC 5A D4   4E 25 6A 98 45 B2 50 E4  0,..-.Z.N%j.E.P.
0010: 27 17 E4 77 E8 65 3F D3   02 14 7A 68 A8 42 12 39  '..w.e?...zh.B.9
0020: 56 0B 46 E9 D3 FD B7 F3   B7 EF 40 A6 D6 E4        V.F.......@...

]

Batch Scripting

Miscellaneous

Advanced Topics

Custom Scripting

With extendable script system, you can provide interactive monitor and control. Is that true? Let's see an example:

public class HelloScriptFactory implements ScriptFactory {
    @Override
    public Script createScript() {
        return new HelloScript();
    }
}

public class HelloScript implements Script {
    private ScriptContext context;

    @Override
    public void setScriptContext(ScriptContext context) {
        this.context = context;
    }

    @ScriptUsage(description = "say hello", 
                 arguments = {@ScriptArgument(name = "name", description = "to who?")})
    public void say(String[] args) {
        String name = args[0];
        context.println("hello " + name);
    }

    // all other methods that have an String[] argument can be called as command.
}

and small iPOJO metadata.xml configuration (or you can use annotation)

<ipojo>
...
	<component classname="org.krakenapps.tutorial.HelloScriptFactory" name="hello-script-factory">
		<provides>
			<property name="alias" type="string" value="hello" />
		</provides>
	</component>
	<instance component="hello-script-factory" />
...
</ipojo> 

That's all. Then if you type hello.say xeraph on kraken console, hello xeraph will be printed out. You already documented command with @ScriptUsage annotation (it is optional), so usage will be printed if you didn't set name argument.

It seems like an operating system because you can monitor and control all aspects of the system using the shell, and even more, you can build up your own applications or commands.

Kraken Package System

At this moment, you may think about OSGi bundle management problem. We all use package system or at least use installer in conventional operating system. No one manages countless executable binaries (dll or exe) by hands. Kraken Core provides own package system and you can install package by just one-line command. For example, pkg.install kraken-syslog will download and install syslog related bundles from package repository. Specified bundles are automatically started and provisioned. Of course, you can make your own package repository and use it. Refer package system? page for details.

Instrumentation Framework

  • You can use  Intrumentation framework introduced in Java 5.
  • Support since kraken-core 1.3.4
  • Start kraken core with -javaagent option.
    java -javaagent:kraken-core-1.3.4-package.jar -jar kraken-core-1.3.4-package.jar
    
  • Get IntrumentationService OSGi service using bundle context or iPOJO require configuration. (org.krakenapps.api.InstrumentationService)
    ServiceReference ref = bundleContext.getServiceReference(InstrumentationService.class.getName());
    InstrumentationService instService = bundleContext.getService(ref);
    
    .. or ..
    
    public class SampleScriptFactory implements ScriptFactory {
        @Requires 
        // you can also use <requires field="instService" /> at component declaration in iPOJO metadata.xml
        private InstrumentationService instService;
    }
    
  • Use InstrumentationService object.
       // inst can be null if you did not start with javaagent option 
       // or JVM does not support instrumentation.
       Instrumentation inst = instService.getInstrumentation();
       long size = inst.getObjectSize(obj);
    

Windows Installer Packaging

Run as a Windows Service

Materials

You must use latest kraken core 1.7.0 binary.

See following materials first.

Install

S:\srcs\kraken\kraken-core\target>
  svc\amd64\prunsrv.exe 
    //IS//"Kraken_Core" 
    --Install=S:\srcs\kraken\kraken-core\target\svc\amd64\prunsrv.exe
    --Description="Kraken core 1.7.0"
    --Jvm=%JAVA_HOME%\jre\bin\server\jvm.dll
    --StartMode=jvm
    --Classpath=%CLASSPATH%;S:\srcs\kraken\kraken-core\target\kraken-core-1.7.0-package.jar
    --StartClass=org.krakenapps.main.Kraken
    --StartMethod=windowsService
    --StartParams=start
    --StopMode=jvm
    --StopClass=org.krakenapps.main.Kraken
    --StopMethod=windowsService
    --StopParams=stop
    --LogPath=S:\srcs\kraken\kraken-core\target\svc
    --StdError=auto
    --StdOutput=auto
    --StartPath=S:\srcs\kraken\kraken-core\target

Major options:

  • Path of prunsrv (check your machine architecture)
  • --Install
  • --Jvm
  • --Classpath
  • --LogPath
  • --StartPath

You can also use --Startup option (see procrun manual)

Ensure you CANNOT use whitespace in service name (argument right after "IS"). (in commons-daemon 1.0.4)

Uninstall

svc\amd64\prunsrv //DS//"Kraken_Core"

See also

Authors

Release History

  • 2.0.0 TODO
    • add script argument auto-completion, and add path auto-completion helper
    • add ssh port option (and change kraken.port to kraken.telnet.port)
    • fix double-quote and backslash escape
    • add os shell for linux and windows
    • add full featured editor
    • replace all prefsvc related code with confdb
    • embed kraken-cron and remove separate log cleaner thread
    • add bundle.update to support version upgrade (no bundle id change)
    • add local/external auth system (with two-factor auth)
    • add no-auth option and --help option (usage print)
    • add external script execution (i.e. os shell scripting for kraken)
    • automated rpm build and msi build
    • full documentation
  • 1.9.0
    • added kraken-confdb and conf.* commands
    • added "scp" (to) command
    • added "set" command and environment variable support to keystore commands
    • updated "bundle.update" command to download new version for bundle which is installed from remote maven repository
  • 1.8.0
    • upgraded apache felix to 3.2.2 (resolved zip file closed exception)
  • 1.7.0 ~ 1.7.8
    • Breaking Changes
      • added ScriptContext.println(Object) and ScriptContext.print(Object)
      • changed default parent directory of 'cache', 'download' to executing jar directory instead of 'user.home' or 'user.dir' working directory
    • Minor Changes
      • added thread cpu usage monitoring
      • added FRAMEWORK_BOOTDELEGATION for JProfiler
      • added interface for prunsrv (apache commons daemon)
      • added "bundle.timestamp" and "bundle.manifest" command
      • added "pkg.installables" command for package browsing
      • added "pkg.export" command for package description generation
      • added "bundle.downloadroot" command
      • added local repository support to MavenMetadata resolving
      • added "-d" start option for developers (update all local bundles before starting felix framework)
      • added hostname to prompt
      • added ls, cd, mv, cp, rm, wget, scp (from) commands
      • added -Dkraken.dir option (change base directory of cache, log, download)
      • added control key support (e.g. CTRL-S) and simple "edit" command
    • Fixed Bugs
      • fixed keystore file handle leak
      • fixed infinite exception loop after disconnect ssh terminal
      • fixed infinite input blocking caused by ESCAPE sequence
  • 1.6.0
    • Breaking Changes
      • added ScriptContext.printf instead of ScriptContext.println
      • removed hsqldb dependency (incompatible with old kraken bundles)
    • Major Changes
      • support ssh shell and user authentication
      • support http basic authentication and https for bundle/package management
      • support local bundle/package repository ( file:// scheme)
      • support keystore management
      • support thread stack monitoring and performance monitoring
      • support SIGTERM signal and shutdown command
      • support automatic kraken log purging (retains 1week data)
      • support window size changed event
    • Minor Changes
      • support bundle repository priority
      • added bundle.restart and bundle.updateAll command
      • added debug logging switch (logger.on and logger.off commands)
      • added some embedded command (date, guid, color, ipconfig)
      • updated felix framework version to 2.0.5
      • added more system packages (org.w3c.dom.css, javax.xml.datatype)
      • added ScriptContext.readPassword()
      • chaged default log directory
  • 1.5.0 SNAPSHOT
    • Added SSH console and login support
      • Telnet console will be deprecated in a future release
    • Added HTTP-Auth for package system
    • Added bundle.restart command
    • Added priority for bundle repository
    • Removed maven bundle dependency resolver
  • 1.4.3
  • 1.0.0