Jarek Przygódzki. Blog programisty

Ogólne przemyślenia na temat rzemiosła / sztuki / nauki programowania i tematów pokrewnych.

Lokalizacja (wersje językowe) komunikatów o błędach z wbudowanego w JRE silnika JavaScript-u w OSGi z wykorzystaniem extension:=extclasspath

leave a comment »

Aplikacja nad którą pracuję umożliwia użytkownikom rozszerzenia funkcjonalności przez pisanie skryptów w JS (szczegóły nie są istotne). Oczywiście, gdy pisze się kod pojawiają się komunikaty o błędach a wymaganiem klienta było by były w języku polskim. Ponieważ domyślnie dostępne są dwie wersje językowe – angielska i francuska – stworzony został plik sun/org/mozilla/javascript/internal/resources/Messages_pl.properties z polskimi komunikatami który następnie został opakowany w plik jar sun.org.mozilla.javascript.nl.PL_pl.jar. W końcu plik ten został przekazany jako rozszerzenie bootclasspath aplikacji za pomocą opcji

-Xbootclasspath/a:lib\sun.org.mozilla.javascript.nl.PL_pl.jar

i rozwiązanie takie działo przed dłuższy czas (także w OSGi).

W trakcie migracji na Equinox p2 okazało się jednak, że z wielu względow -Xbootclasspath/a musi odejść – a mnie przypadło wymyślenie jak.

Pierwszym krokiem jest oczywiście ustalenie, dlaczego działało dotychczasowe rozwiązanie. Analiza wykazała, że komunikaty wczytywane są w klasie sun.org.mozilla.javascript.internal.ScriptRuntime wywołaniem

ResourceBundle.getBundle("sun.org.mozilla.javascript.internal.resources.Messages", locale)

a oryginalne lokalizacje (angielska i francuska) umieszczone są w <jre>/lib/resources.jar. Dalsza analiza wykazała, że w tym przypadku do wczytywania lokalizacji wykorzystywany jest tzw. classloader aplikacji. Żeby zrozumieć, dlaczego wyszukiwanie lokalizacji powodzi się trzeba pamiętać o trzech podstawowych faktach dotyczących classloader-ów

  • są zorganizowane są w strukturę drzewiastą i powiązane relacją dziecko-rodzic
  • zwykle przed wczytaniem klasy\zasobu samemu następuje próba delegacji do rodzica (parent-first delegation model – ale model child-first też jest popularny. Ważne jest to, że classloader „widzi” klasy i zasoby swoje i przodków – ale nie potomków
  • główne classloadery (boot, ext, app) zorganizowane są następującojvm-classloader-hierarchy

Podczas wyszukiwania zasobu sun/org/mozilla/javascript/internal/resources/Messages_pl.properties Application Classloader deleguje do Extension ClassLoader który z kolei deleguje do Boostrap ClassLoader który znajduje zasób dzięki -Xbootclasspath – i voilà , mamy polskie komunikaty.

Dla osób dobrze znających OSGi naturalnym sposobem na rozwiązanie przyjazne dla OSGi wydają się tzw. framework extensions czyli pakunki częściowe (fragmenty) których hostem jest pakunek systemowy – system.bundle – i które dodatkowo definiują tzw. punkt rozszerzeń. Zgodnie ze specyfikacją OSGi poprawne są dwie wartości

  • framework
  • bootclasspath

opisane np. tutaj i w specyfikacji OSGi
Pierwszy jest dla nas bezużyteczny (pytanie dla uważnych – dlaczego). Drugi wydaje się idealny jako odpowiednik parametru -Xbootclasspath ale w praktyce okazuje się, że frameworki OSGI nie implementują go z przyczyn technicznych (patrz np. Eclipse/127724). Co nam pozostaje? Okazuje się, że Equinox OSGi obsługuje niestandardowy punkt rozszerzeń który idealnie sprawdza się w tym przypadku. Można bowiem stworzyć fragment który rozszerzy classloader aplikacji korzystając ze składni Fragment-Host: system.bundle; extension:=extclasspath. Wystarczy stworzyć fragment z następującym manifestem

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Mozilla Rhino Localization PL
Bundle-SymbolicName: sun.org.mozilla.javascript.nl.PL_pl
Bundle-Version: 1.0.0.qualifier
Fragment-Host: system.bundle; extension:=extclasspath
Bundle-RequiredExecutionEnvironment: JavaSE-1.6

który zawiera zasób sun/org/mozilla/javascript/internal/resources/Messages_pl.properties

Jak Equinox OSGi obsługuje framework extensions

czyli jak mawia Linus Torvalds Talk is cheap. Show me the code. W tym przypadku kluczowe są dwa miejsca. Pierwszym jest metoda org.eclipse.osgi.internal.baseadaptor.BaseStorageHook.loadManifest gdzie wczytywany jest manifest pakunku

static void loadManifest(BaseData target, Dictionary<String, String> manifest) throws BundleException {
	try {
		target.setVersion(Version.parseVersion(manifest.get(Constants.BUNDLE_VERSION)));
	} catch (IllegalArgumentException e) {
		target.setVersion(new InvalidVersion(manifest.get(Constants.BUNDLE_VERSION)));
	}
	ManifestElement[] bsnHeader = ManifestElement.parseHeader(Constants.BUNDLE_SYMBOLICNAME, manifest.get(Constants.BUNDLE_SYMBOLICNAME));
	int bundleType = 0;
	if (bsnHeader != null) {
		target.setSymbolicName(bsnHeader[0].getValue());
		String singleton = bsnHeader[0].getDirective(Constants.SINGLETON_DIRECTIVE);
		if (singleton == null)
			singleton = bsnHeader[0].getAttribute(Constants.SINGLETON_DIRECTIVE);
		if ("true".equals(singleton)) //$NON-NLS-1$
			bundleType |= BundleData.TYPE_SINGLETON;
	}
	// check that the classpath is valid
	String classpath = manifest.get(Constants.BUNDLE_CLASSPATH);
	ManifestElement.parseHeader(Constants.BUNDLE_CLASSPATH, classpath);
	target.setClassPathString(classpath);
	target.setActivator(manifest.get(Constants.BUNDLE_ACTIVATOR));
	String host = manifest.get(Constants.FRAGMENT_HOST);
	if (host != null) {
		bundleType |= BundleData.TYPE_FRAGMENT;
		ManifestElement[] hostElement = ManifestElement.parseHeader(Constants.FRAGMENT_HOST, host);
		if (Constants.getInternalSymbolicName().equals(hostElement[0].getValue()) || Constants.SYSTEM_BUNDLE_SYMBOLICNAME.equals(hostElement[0].getValue())) {
			String extensionType = hostElement[0].getDirective("extension"); //$NON-NLS-1$
			if (extensionType == null || extensionType.equals("framework")) //$NON-NLS-1$
				bundleType |= BundleData.TYPE_FRAMEWORK_EXTENSION;
			else if (extensionType.equals("bootclasspath")) //$NON-NLS-1$
				bundleType |= BundleData.TYPE_BOOTCLASSPATH_EXTENSION;
			else if (extensionType.equals("extclasspath")) //$NON-NLS-1$
				bundleType |= BundleData.TYPE_EXTCLASSPATH_EXTENSION;
		}
	} else {
		String composite = manifest.get(COMPOSITE_HEADER);
		if (composite != null) {
			if (COMPOSITE_BUNDLE.equals(composite))
				bundleType |= BundleData.TYPE_COMPOSITEBUNDLE;
			else
				bundleType |= BundleData.TYPE_SURROGATEBUNDLE;
		}
	}
	target.setType(bundleType);
	target.setExecutionEnvironment(manifest.get(Constants.BUNDLE_REQUIREDEXECUTIONENVIRONMENT));
	target.setDynamicImports(manifest.get(Constants.DYNAMICIMPORT_PACKAGE));
}

drugim jest metoda org.eclipse.osgi.internal.baseadaptor.BaseStorage.processExtension gdzie dane rozszerzenie jest obsługiwane

protected void processExtension(BaseData bundleData, byte type) throws BundleException {
	if ((bundleData.getType() & BundleData.TYPE_FRAMEWORK_EXTENSION) != 0) {
		validateExtension(bundleData);
		processFrameworkExtension(bundleData, type);
	} else if ((bundleData.getType() & BundleData.TYPE_BOOTCLASSPATH_EXTENSION) != 0) {
		validateExtension(bundleData);
		processBootExtension(bundleData, type);
	} else if ((bundleData.getType() & BundleData.TYPE_EXTCLASSPATH_EXTENSION) != 0) {
		validateExtension(bundleData);
		processExtExtension(bundleData, type);
	}
}

Written by Jarek Przygódzki

Styczeń 15, 2014 at 9:18 pm

[Java] Quartz i pule o zmiennej ilości wątków.

leave a comment »

Quartz to planista zadań powszechnie wykorzystywany w aplikacjach Java i .NET. Jednym z poważnych ograniczeń w wykorzystaniu Quartza na dużą skalę jest domyślna implementacji puli wątków – org.quartz.simpl.SimpleThreadPool która tworzy podczas inicjalizacji pulę wątków o stały rozmiarze i nigdy nie zwalnia nieużywanych wątków. Takie zachowanie ma sens gdy pula jest bardzo mocno obciążona i wątki są cały czas wykorzystywane, ale w praktyce to rozwiązanie się nie sprawdza – dlaczego mamy utrzymywać 1000 wątków tylko dlatego że są potrzebne raz na tydzień? Ponieważ domyślnie Quartz nie ma odpowiedniej implementacji puli wątków zmuszony byłem napisać własną

Pula ta z jednej strony zapewnia ponowne wykorzystanie wątków gdy system jest obciążony (unikając tym samym kosztów wielokrotnego tworzenia wątków – Why is creating a Thread said to be expensive?) a jednocześnie zwalnia wątki które były nieaktywne przez odpowiednio długi czas unikając tym samym niepotrzebnego wykorzystania pamięci natywnej i PermGen-u.

Written by Jarek Przygódzki

Grudzień 16, 2013 at 7:56 pm

Napisane w Java

Tagi: ,

Windows – Jak zobaczyć co blokuje nam dany plik

leave a comment »

Jednym z najbardziej frustrujących momentów dla użytkownika Windows jest chwila, gdy podczas próby usunięcie pliku dowiaduje się, że nie może tego zrobić gdyż jest on używany przez inny proces („Proces nie może uzyskać dostępu do pliku, ponieważ jest on używany przez inny proces”). Pomijają absurd tej sytuacji (Linux\OS X nie mają takich ograniczeń) w komunikacie brakuje bardzo istotnej informacji – jaki proces?.

Sposobów na uzyskanie tej informacji jest kilka, moim ulubionym jest Process Explorer. Po uruchomieniu aplikacji (która u mnie i u wielu programistów których znam zastępuje domyślnego zarządcę zadań) wybieramy File → Find Handle or DLL (Ctrl+F), wpisujemy nazwę pliku który nas interesuje i od razu widzimy który proces ma otwarty dany plik.

proceexp

Możemy teraz łatwo za pomocą opcji Kill Process/Close Handle wymusić zamknięcie danego pliku lub całego procesu.

Written by Jarek Przygódzki

Grudzień 11, 2013 at 7:46 pm

Napisane w tools

Tagi: ,

OOM w Jetty z powodu zbyt dużej pamięci podręcznej zasobów (org.mortbay.jetty.ResourceCache$Content)

leave a comment »

Niedawno zaczęliśmy wykorzystywać Jetty do hostowania repozytoriów P2. Szybko okazało się, że dotychczasowy rozmiar sterty który jak się wydawało w zupełności wystarczał do udostępniania statycznych zasobów (-Xmx256M) okazał się niewystarczający – podczas materializacji produktu P2 z repozytorium proces Jetty kończył się wyjątkiem java.lang.OutOfMemoryError. Doraźnym rozwiązaniem było podwojenie XMX, ale w końcu zrobiony automatycznie zrzut pamięci doczekał się analizy. Analiza to zresztą za dużo powiedziane, bo nigdy jeszcze nie widziałem tak ewidentnej przyczyny – po otwarciu zrzutu pamięci w Eclipse Memory Analyzer (MAT) raport Leak Suspects okazał się aż nadto jednoznaczny.

jetty_oom_leak_suspects

Co się stało? Okazuje się, że domyślne wartości w klasie org.mortbay.jetty.ResourceCache są bardzo konserwatywne

// jetty-server/src/main/java/org/eclipse/jetty/server/ResourceCache.java
    private int _maxCachedFileSize =4*1024*1024;
    private int _maxCachedFiles=2048;
    private int _maxCacheSize =32*1024*1024;

ale te w w bazowej konfiguracji domyślnego servletu już nie

   <init-param>
      <param-name>maxCacheSize</param-name>
      <param-value>256000000</param-value>
    </init-param>
    <init-param>
      <param-name>maxCachedFileSize</param-name>
      <param-value>200000000</param-value>
    </init-param>
    <init-param>
      <param-name>maxCachedFiles</param-name>
      <param-value>2048</param-value>
    </init-param>

Spodziewałbym się tutaj chociaż szczątkowej heurystyki – przeznaczenie 256000000 bajtów na pamięć podręczną gdy całkowity rozmiar stery jest niewiele większy (lub mniejszy) nie jest zbyt rozsądną decyzją. Trzymanie w pamięci 200M plików też wydaje się nieco dziwne. Rozwiązaniem jest ustawienie parametru maxCacheSize w WEB-INF/web.xml na wartość o tyle mniejszą od XMX by wystarczyło pamięci do normalnej pracy aplikacji

    <servlet>
    <servlet-name>default</servlet-name>
    <servlet-class>org.mortbay.jetty.servlet.DefaultServlet</servlet-class>
    <init-param>
      <param-name>maxCacheSize</param-name>
      <!-- 128M - wartość MUSI być mniejsza od XMX -->
      <param-value>134217728</param-value>
    </init-param>
    <init-param>
      <param-name>maxCachedFileSize</param-name>
      <!-- Nie umieszczaj w pamięci podręcznej plików większych niż 32M -->	  
      <param-value>33554432</param-value>
    </init-param>
   </servlet>

Wartość 32M jest empiryczna, przed jej dobraniem sprawdziłem rozmiar największych plików JAR w repozytorium poleceniem (Cygwin is Fun in Terminal)

$ find plugins -name "*.jar"  -type f | xargs ls -lhS | head -n 10

Written by Jarek Przygódzki

Listopad 21, 2013 at 7:42 pm

Napisane w JVM

Tagi: , , ,

Niekonsekwentna obsługa manifestu przez JarFile i JarInputStream

leave a comment »

Napisałem niedawno program narzędziowy który przeszukiwał bundle OSGi w poszukiwaniu dynamicznych importów i deklaracji buddy class loading. Kod wczytujący manifest z pliku JAR wyglądał mniej więcej tak

import java.util.jar.*

def withJarInputStream(InputStream inputStream) {
    def jarIn
    try {
        jarIn = new JarInputStream(inputStream)
        closure(jarIn)
    } finally {
        jarIn?.close()
    }
}

def readManifest(File jar) {
    jar.withInputStream { inputStream ->
        withJarInputStream(inputStream) { jarIn ->
            Manifest mf = jarIn.getManifest()
            return mf
        }
    }
}

Okazało się jednak, że w niektórych plikach JAR nie potrafi on odnaleźć istniejącego manifestu podczas gdy konstrukcja

import java.util.jar.*

def withJar(File file) {
    def jarFile
    try {
        jarFile = new JarFile(file)
        closure(jarIn)
    } finally {
        jarFile?.close()
    }
}

def readManifest(File file) {
    withJar(file) { jarFile ->
        Manifest mf = jarFile.getManifest()
        return mf
    }
}

działała poprawnie! Zaciekawiony postanowiłem udać się do źródeł i sytuacja stała się jasna. Klasa java.util.jar.JarInputStream poszukuje pliku JarFile.MANIFEST_NAME tylko w dwóch pierwszych rekordach strumienia ZipInputStream

   // java.util.jar.JarInputStream.JarInputStream(InputStream, boolean)
   public JarInputStream(InputStream in, boolean verify) throws IOException {
        super(in);
        this.doVerify = verify;

        // This implementation assumes the META-INF/MANIFEST.MF entry
        // should be either the first or the second entry (when preceded
        // by the dir META-INF/). It skips the META-INF/ and then
        // "consumes" the MANIFEST.MF to initialize the Manifest object.
        JarEntry e = (JarEntry)super.getNextEntry();
        if (e != null && e.getName().equalsIgnoreCase("META-INF/"))
            e = (JarEntry)super.getNextEntry();
        first = checkManifest(e);
    }

    private JarEntry checkManifest(JarEntry e)
        throws IOException
    {
        if (e != null && JarFile.MANIFEST_NAME.equalsIgnoreCase(e.getName())) {
            man = new Manifest();
            byte bytes[] = getBytes(new BufferedInputStream(this));
            man.read(new ByteArrayInputStream(bytes));
            closeEntry();
            if (doVerify) {
                jv = new JarVerifier(bytes);
                mev = new ManifestEntryVerifier(man);
            }
            return (JarEntry)super.getNextEntry();
        }
        return e;
    }

podczas gdy java.util.jar.JarFile przeszukuje całą zawartość archiwum

    // java.util.jar.JarFile.getManEntry()
    private JarEntry getManEntry() {
        if (manEntry == null) {
            // First look up manifest entry using standard name
            manEntry = getJarEntry(MANIFEST_NAME);
            if (manEntry == null) {
                // If not found, then iterate through all the "META-INF/"
                // entries to find a match.
                String[] names = getMetaInfEntryNames();
                if (names != null) {
                    for (int i = 0; i &lt; names.length; i++) {
                        if (MANIFEST_NAME.equals(
                                                 names[i].toUpperCase(Locale.ENGLISH))) {
                            manEntry = getJarEntry(names[i]);
                            break;
                        }
                    }
                }
            }
        }
        return manEntry;
    }

Inną kwestią jest pochodzenie takich nietypowych plików JAR; przypuszczam że powstały w wyniku ręcznej modyfikacji manifestu archiwum JAR za pomocą narzędzi typu 7-Zip.

Written by Jarek Przygódzki

Listopad 7, 2013 at 10:04 pm

Czyszczenie przestrzenie robocze w Eclipse

leave a comment »

Skrypt czyszczący projekty w workspace-ach Eclipse który uruchamiam automatycznie przed każdym backupem


def path = '/Users/Jarek/Code/Eclipse/Workspaces'

def isProject = ~/.*\.project/

def dryRun = false

def timeIt = {String message, Closure cl ->
    def startTime = System.currentTimeMillis()
    cl()
    def deltaTime = System.currentTimeMillis() - startTime
    println "$message: \t: $deltaTime ms" 
}


def toRemove = [] 

new File(path).eachDirRecurse { dir->
    dir.eachFileMatch(isProject) { f ->
        def cp = new File(dir, '.classpath')
        if(cp.exists()) {
            def classpath = new XmlSlurper().parse(cp)
            def bins = classpath.classpathentry.
                findAll { it.@kind == 'output' }.
                collect { new File(dir, it.@path.text()) }.
                findAll { it.exists() }
            toRemove.addAll bins
        }
    }
}


if(dryRun) {
   println 'Removing (dry run)'
   println '------------------'
   toRemove.each { println it.getPath() }
   
} else {
    println 'Removing'
    println '--------'
    toRemove.each { f->
        timeIt("${f.getPath()}") { 
            f.deleteDir()
        }
    }
}
echo 'Done'

Written by Jarek Przygódzki

Listopad 6, 2013 at 8:32 pm

Napisane w Groovy

JOGL w Eclipse RCP

leave a comment »

W ostatnim czasie w ramach eksperymentów z Tycho dokonałem konwersji binarek JOGL udostępnianych przez JogAmp do postaci umożliwiającej ich łatwe wykorzystanie w aplikacjach opartych o Eclipse RCP oraz napisałem proste demo

jog-rcp-demo

Źrodła obu projektów znajdują się na moim koncie w serwisie GitHub

Written by Jarek Przygódzki

Październik 2, 2013 at 10:07 pm

Napisane w Java, Tycho

Tagi: , , , , ,

Follow

Otrzymuj każdy nowy wpis na swoją skrzynkę e-mail.