diff --git a/pom.xml b/pom.xml index 2678a55..46a03f1 100644 --- a/pom.xml +++ b/pom.xml @@ -73,8 +73,8 @@ org.slf4j slf4j-log4j12 1.7.7 - test + diff --git a/src/main/java/com/microsoft/aad/msal4jextensions/CrossProcessCacheFileLock.java b/src/main/java/com/microsoft/aad/msal4jextensions/CrossProcessCacheFileLock.java index 34636c7..52e3541 100644 --- a/src/main/java/com/microsoft/aad/msal4jextensions/CrossProcessCacheFileLock.java +++ b/src/main/java/com/microsoft/aad/msal4jextensions/CrossProcessCacheFileLock.java @@ -5,35 +5,34 @@ import java.io.File; import java.io.IOException; -import java.io.RandomAccessFile; import java.lang.management.ManagementFactory; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * Cross process lock based on OS level file lock. + * Cross process lock based on lock file creation/deletion (optional) + OS level file lock. */ class CrossProcessCacheFileLock { - private final static Logger LOG = LoggerFactory.getLogger(CrossProcessCacheFileLock.class); - public static final String READ_MODE = "r"; - public static final String WRITE_MODE = "rw"; private int retryDelayMilliseconds; private int retryNumber; private File lockFile; - private RandomAccessFile randomAccessFile; - - private String mode; private FileLock lock; + private FileChannel fileChannel; + + private boolean locked; + /** * Constructor * @@ -43,113 +42,134 @@ class CrossProcessCacheFileLock { */ CrossProcessCacheFileLock(String lockfileName, int retryDelayMilliseconds, int retryNumber) { lockFile = new File(lockfileName); - lockFile.deleteOnExit(); this.retryDelayMilliseconds = retryDelayMilliseconds; this.retryNumber = retryNumber; } - /** - * Acquire read lock - can be shared by multiple readers - * - * @throws CacheFileLockAcquisitionException - */ - void readLock() throws CacheFileLockAcquisitionException { - lock(READ_MODE); - } - - /** - * Acquire write lock - exclusive access - * - * @throws CacheFileLockAcquisitionException - */ - void writeLock() throws CacheFileLockAcquisitionException { - lock(WRITE_MODE); - } - - private String getProcessId(){ + private String getProcessId() { String vmName = ManagementFactory.getRuntimeMXBean().getName(); - String pid = vmName.substring(0, vmName.indexOf("@")); - - return pid; + return vmName.substring(0, vmName.indexOf("@")); } private String getLockProcessThreadId() { return "pid:" + getProcessId() + " thread:" + Thread.currentThread().getId(); } + private boolean tryToCreateLockFile() { + for (int tryCount = 0; tryCount < retryNumber; tryCount++) { + boolean fileCreated = false; + try { + fileCreated = lockFile.createNewFile(); + } catch (IOException ex) { + LOG.error(ex.getMessage()); + } + if (fileCreated) { + return true; + } else { + waitBeforeRetry(); + } + } + return false; + } + + private void waitBeforeRetry(){ + try { + Thread.sleep(retryDelayMilliseconds); + } catch (InterruptedException e) { + LOG.error(e.getMessage()); + } + } + /** - * Tries to acquire OS lock for lock file + * Tries to acquire lock by creating lockFile (optional), + * and acquiring OS lock for lockFile (mandatory) * Retries {@link #retryNumber} times with {@link #retryDelayMilliseconds} delay * * @throws CacheFileLockAcquisitionException if the lock was not obtained. */ - private void lock(String mode) throws CacheFileLockAcquisitionException { + void lock() throws CacheFileLockAcquisitionException { + if (!tryToCreateLockFile()) { + LOG.debug(getLockProcessThreadId() + " Failed to create lock file!"); + } + for (int tryCount = 0; tryCount < retryNumber; tryCount++) { try { lockFile.createNewFile(); - LOG.debug(getLockProcessThreadId() + " acquiring " + mode + " file lock"); - - randomAccessFile = new RandomAccessFile(lockFile, mode); - FileChannel channel = randomAccessFile.getChannel(); + LOG.debug(getLockProcessThreadId() + " acquiring file lock"); + fileChannel = FileChannel.open(lockFile.toPath(), + StandardOpenOption.READ, + StandardOpenOption.SYNC, + StandardOpenOption.WRITE); - boolean isShared = READ_MODE.equals(mode); - lock = channel.lock(0L, Long.MAX_VALUE, isShared); - - this.mode = mode; + // try to get file lock + lock = fileChannel.tryLock(); + if (lock == null) { + throw new IllegalStateException("Lock is not available"); + } - if (!lock.isShared()) { - String jvmName = java.lang.management.ManagementFactory.getRuntimeMXBean().getName(); + // for debugging purpose write jvm name to lock file + writeJvmName(fileChannel); - // for debugging purpose - // if exclusive lock acquired, write jvm name to lock file - ByteBuffer buff = ByteBuffer.wrap(jvmName.replace("@", " "). - getBytes(StandardCharsets.UTF_8)); - channel.write(buff); - } - LOG.debug(getLockProcessThreadId() + " acquired file lock, isShared - " + lock.isShared()); + locked = true; + LOG.debug(getLockProcessThreadId() + " acquired OK file lock"); return; } catch (Exception ex) { - LOG.debug(getLockProcessThreadId() + " failed to acquire " + mode + " lock," + + LOG.debug(getLockProcessThreadId() + " failed to acquire lock," + " exception msg - " + ex.getMessage()); - try { - releaseResources(); - } catch (IOException e) { - LOG.error(e.getMessage()); - } - - try { - Thread.sleep(retryDelayMilliseconds); - } catch (InterruptedException e) { - LOG.error(e.getMessage()); - } + releaseResources(); + waitBeforeRetry(); } } - LOG.error(getLockProcessThreadId() + " failed to acquire " + mode + " lock"); + LOG.error(getLockProcessThreadId() + " failed to acquire lock"); throw new CacheFileLockAcquisitionException( - getLockProcessThreadId() + " failed to acquire " + mode + " lock"); + getLockProcessThreadId() + " failed to acquire lock"); + } + + private void writeJvmName(FileChannel fileChannel) throws IOException { + String jvmName = ManagementFactory.getRuntimeMXBean().getName(); + + ByteBuffer buff = ByteBuffer.wrap(jvmName.replace("@", " "). + getBytes(StandardCharsets.UTF_8)); + fileChannel.write(buff); } /** - * Release OS lock for lockFile + * Release OS lock for lockFile, + * delete lockFile if it was created by lock() method * * @throws IOException */ void unlock() throws IOException { - LOG.debug(getLockProcessThreadId() + " releasing " + mode + " lock"); + LOG.debug(getLockProcessThreadId() + " releasing lock"); releaseResources(); + + if (locked) { + deleteLockFile(); + } } - private void releaseResources() throws IOException { - if (lock != null) { - lock.release(); + private void deleteLockFile() throws IOException { + if (!Files.deleteIfExists(lockFile.toPath())) { + LOG.debug(getLockProcessThreadId() + " FAILED to delete lock file"); + } + } + + private void releaseResources() { + try { + if (lock != null) { + lock.release(); + } + if (fileChannel != null) { + fileChannel.close(); + } } - if (randomAccessFile != null) { - randomAccessFile.close(); + catch (IOException ex){ + LOG.error(ex.getMessage()); } } } \ No newline at end of file diff --git a/src/main/java/com/microsoft/aad/msal4jextensions/PersistenceSettings.java b/src/main/java/com/microsoft/aad/msal4jextensions/PersistenceSettings.java index fd64949..8d518a4 100644 --- a/src/main/java/com/microsoft/aad/msal4jextensions/PersistenceSettings.java +++ b/src/main/java/com/microsoft/aad/msal4jextensions/PersistenceSettings.java @@ -312,7 +312,12 @@ public Builder setLockRetry(int delayMilliseconds, int retryNumber) { * @return An immutable instance of {@link com.microsoft.aad.msal4jextensions.PersistenceSettings}. */ public PersistenceSettings build() { - PersistenceSettings persistenceSettings = new PersistenceSettings( + if (keyringSchemaName != null && linuxUseUnprotectedFileAsCacheStorage) { + throw new IllegalArgumentException( + "Only one type of persistence can be used on Linux - KeyRing or Unprotected file"); + } + + return new PersistenceSettings( cacheFileName, cacheDirectoryPath, keychainService, @@ -327,8 +332,6 @@ public PersistenceSettings build() { linuxUseUnprotectedFileAsCacheStorage, lockRetryDelayMilliseconds, lockRetryNumber); - - return persistenceSettings; } } } diff --git a/src/main/java/com/microsoft/aad/msal4jextensions/PersistenceTokenCacheAccessAspect.java b/src/main/java/com/microsoft/aad/msal4jextensions/PersistenceTokenCacheAccessAspect.java index fe86e56..cd8850d 100644 --- a/src/main/java/com/microsoft/aad/msal4jextensions/PersistenceTokenCacheAccessAspect.java +++ b/src/main/java/com/microsoft/aad/msal4jextensions/PersistenceTokenCacheAccessAspect.java @@ -35,7 +35,7 @@ public class PersistenceTokenCacheAccessAspect implements ITokenCacheAccessAspec private PersistenceSettings parameters; private String getCacheLockFilePath() { - return parameters.getCacheDirectoryPath() + File.separator + ".lock"; + return parameters.getCacheDirectoryPath() + File.separator + ".lockfile"; } private String getCacheFilePath() { @@ -94,7 +94,7 @@ private boolean isReadAccess(ITokenCacheAccessContext iTokenCacheAccessContext) return !isWriteAccess(iTokenCacheAccessContext); } - private void updateLastSeenCacheFileModifiedTimestamp() throws IOException { + private void updateLastSeenCacheFileModifiedTimestamp() { lastSeenCacheFileModifiedTimestamp = getCurrentCacheFileModifiedTimestamp(); } @@ -106,14 +106,14 @@ public Long getCurrentCacheFileModifiedTimestamp() { public void beforeCacheAccess(ITokenCacheAccessContext iTokenCacheAccessContext) { try { if (isWriteAccess(iTokenCacheAccessContext)) { - lock.writeLock(); + lock.lock(); } else { Long currentCacheFileModifiedTimestamp = getCurrentCacheFileModifiedTimestamp(); if (currentCacheFileModifiedTimestamp != null && currentCacheFileModifiedTimestamp.equals(lastSeenCacheFileModifiedTimestamp)) { return; } else { - lock.readLock(); + lock.lock(); } } byte[] data = cacheAccessor.read(); @@ -138,8 +138,6 @@ public void afterCacheAccess(ITokenCacheAccessContext iTokenCacheAccessContext) cacheAccessor.write(iTokenCacheAccessContext.tokenCache().serialize().getBytes(StandardCharset.UTF_8)); updateLastSeenCacheFileModifiedTimestamp(); } - } catch (IOException ex) { - LOG.error(ex.getMessage()); } finally { try { lock.unlock(); diff --git a/src/main/java/com/microsoft/aad/msal4jextensions/persistence/CacheFileAccessor.java b/src/main/java/com/microsoft/aad/msal4jextensions/persistence/CacheFileAccessor.java index 625f16e..7eae6dd 100644 --- a/src/main/java/com/microsoft/aad/msal4jextensions/persistence/CacheFileAccessor.java +++ b/src/main/java/com/microsoft/aad/msal4jextensions/persistence/CacheFileAccessor.java @@ -5,16 +5,23 @@ import com.sun.jna.Platform; import com.sun.jna.platform.win32.Crypt32Util; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileTime; /** * Implementation of CacheAccessor based on File persistence */ public class CacheFileAccessor implements ICacheAccessor { + private final static Logger LOG = LoggerFactory.getLogger(CacheFileAccessor.class); + private String cacheFilePath; private File cacheFile; @@ -47,11 +54,38 @@ public byte[] read() { public void write(byte[] data) { if (Platform.isWindows()) { data = Crypt32Util.cryptProtectData(data); + + try (FileOutputStream stream = new FileOutputStream(cacheFile)) { + stream.write(data); + } + catch (IOException e) { + throw new CacheFileAccessException("Failed to write to Cache File", e); + } + } + else { + writeAtomic(data); } + } - try (FileOutputStream stream = new FileOutputStream(cacheFilePath)) { - stream.write(data); - } catch (IOException e) { + private void writeAtomic(byte[] data) { + File tempFile = null; + try { + try { + tempFile = File.createTempFile("JavaMsalExtTemp", ".tmp", cacheFile.getParentFile()); + + try (FileOutputStream stream = new FileOutputStream(tempFile)) { + stream.write(data); + } + + Files.move(tempFile.toPath(), cacheFile.toPath(), + StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } finally { + if (tempFile != null) { + Files.deleteIfExists(tempFile.toPath()); + } + } + } + catch (IOException e) { throw new CacheFileAccessException("Failed to write to Cache File", e); } } @@ -65,7 +99,13 @@ public void delete() { } } - public void updateCacheFileLastModifiedTimeByWritingDummyData() { - write(new byte[1]); + public void updateCacheFileLastModifiedTime() { + FileTime fileTime = FileTime.fromMillis(System.currentTimeMillis()); + + try { + Files.setLastModifiedTime(Paths.get(cacheFilePath), fileTime); + } catch (IOException e) { + throw new CacheFileAccessException("Failed to set lastModified time on Cache File", e); + } } } diff --git a/src/main/java/com/microsoft/aad/msal4jextensions/persistence/linux/KeyRingAccessor.java b/src/main/java/com/microsoft/aad/msal4jextensions/persistence/linux/KeyRingAccessor.java index d4e9636..439f10d 100644 --- a/src/main/java/com/microsoft/aad/msal4jextensions/persistence/linux/KeyRingAccessor.java +++ b/src/main/java/com/microsoft/aad/msal4jextensions/persistence/linux/KeyRingAccessor.java @@ -48,7 +48,7 @@ public KeyRingAccessor(String cacheFilePath, this.attributeValue2 = attributeValue2; } - public void verify() throws IOException { + public void verify() { String testAttributeValue1 = "testAttr1"; String testAttributeValue2 = "testAttr2"; String testData = "Test Data"; @@ -113,7 +113,7 @@ private void write(byte[] data, String attributeValue1, String attributeValue2) throw new KeyRingAccessException("An error while saving secret to keyring, " + "domain:" + err.domain + " code:" + err.code + " message:" + err.message); } - new CacheFileAccessor(cacheFilePath).updateCacheFileLastModifiedTimeByWritingDummyData(); + new CacheFileAccessor(cacheFilePath).updateCacheFileLastModifiedTime(); } @Override @@ -138,7 +138,7 @@ private void delete(String attributeValue1, String attributeValue2) { throw new KeyRingAccessException("An error while deleting secret from keyring, " + "domain:" + err.domain + " code:" + err.code + " message:" + err.message); } - new CacheFileAccessor(cacheFilePath).updateCacheFileLastModifiedTimeByWritingDummyData(); + new CacheFileAccessor(cacheFilePath).updateCacheFileLastModifiedTime(); } @Override diff --git a/src/main/java/com/microsoft/aad/msal4jextensions/persistence/mac/KeyChainAccessor.java b/src/main/java/com/microsoft/aad/msal4jextensions/persistence/mac/KeyChainAccessor.java index 2ae37d3..6f161ce 100644 --- a/src/main/java/com/microsoft/aad/msal4jextensions/persistence/mac/KeyChainAccessor.java +++ b/src/main/java/com/microsoft/aad/msal4jextensions/persistence/mac/KeyChainAccessor.java @@ -50,8 +50,7 @@ public byte[] read() { } } - @Override - public void write(byte[] data) { + private int writeNoRetry(byte[] data) { Pointer[] itemRef = new Pointer[1]; int status; @@ -62,33 +61,50 @@ public void write(byte[] data) { accountNameBytes.length, accountNameBytes, null, null, itemRef); - if (status != ISecurityLibrary.ERR_SEC_SUCCESS - && status != ISecurityLibrary.ERR_SEC_ITEM_NOT_FOUND) { - throw new KeyChainAccessException(convertErrorCodeToMessage(status)); - } + if (status == ISecurityLibrary.ERR_SEC_SUCCESS && itemRef[0] != null) { - if (itemRef[0] != null) { status = ISecurityLibrary.library.SecKeychainItemModifyContent( itemRef[0], null, data.length, data); - } else { + + } else if (status == ISecurityLibrary.ERR_SEC_ITEM_NOT_FOUND) { status = ISecurityLibrary.library.SecKeychainAddGenericPassword( - Pointer.NULL, + null, serviceNameBytes.length, serviceNameBytes, accountNameBytes.length, accountNameBytes, data.length, data, null); - } - - if (status != ISecurityLibrary.ERR_SEC_SUCCESS) { + } else { throw new KeyChainAccessException(convertErrorCodeToMessage(status)); } - new CacheFileAccessor(cacheFilePath).updateCacheFileLastModifiedTimeByWritingDummyData(); - } finally { if (itemRef[0] != null) { ISecurityLibrary.library.CFRelease(itemRef[0]); } } + return status; + } + + @Override + public void write(byte[] data) { + int NUM_OF_RETRIES = 3; + int RETRY_DELAY_IN_MS = 10; + int status = 0; + + for (int i = 0; i < NUM_OF_RETRIES; i++) { + status = writeNoRetry(data); + + if (status == ISecurityLibrary.ERR_SEC_SUCCESS) { + new CacheFileAccessor(cacheFilePath).updateCacheFileLastModifiedTime(); + return; + } + try { + Thread.sleep(RETRY_DELAY_IN_MS); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + throw new KeyChainAccessException(convertErrorCodeToMessage(status)); } @Override @@ -117,7 +133,7 @@ public void delete() { throw new KeyChainAccessException(convertErrorCodeToMessage(status)); } } - new CacheFileAccessor(cacheFilePath).updateCacheFileLastModifiedTimeByWritingDummyData(); + new CacheFileAccessor(cacheFilePath).updateCacheFileLastModifiedTime(); } finally { if (itemRef[0] != null) { ISecurityLibrary.library.CFRelease(itemRef[0]); diff --git a/src/test/java/com/microsoft/aad/msal4jextensions/CacheFileWriter.java b/src/test/java/com/microsoft/aad/msal4jextensions/CacheFileWriter.java index 0de6366..7cc7632 100644 --- a/src/test/java/com/microsoft/aad/msal4jextensions/CacheFileWriter.java +++ b/src/test/java/com/microsoft/aad/msal4jextensions/CacheFileWriter.java @@ -14,8 +14,10 @@ public static void main(String[] args) throws Exception { lockFilePath = args[1]; filePath = args[2]; + String lockHoldingIntervalsFilePath = args[3]; + CacheFileWriterRunnable cacheFileWriterRunnable = - new CacheFileWriterRunnable(executionId, lockFilePath, filePath); + new CacheFileWriterRunnable(executionId, lockFilePath, filePath, lockHoldingIntervalsFilePath); cacheFileWriterRunnable.run(); } diff --git a/src/test/java/com/microsoft/aad/msal4jextensions/CacheFileWriterRunnable.java b/src/test/java/com/microsoft/aad/msal4jextensions/CacheFileWriterRunnable.java index 365b35b..59a285d 100644 --- a/src/test/java/com/microsoft/aad/msal4jextensions/CacheFileWriterRunnable.java +++ b/src/test/java/com/microsoft/aad/msal4jextensions/CacheFileWriterRunnable.java @@ -3,43 +3,14 @@ package com.microsoft.aad.msal4jextensions; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; +import com.microsoft.aad.msal4jextensions.persistence.CacheFileAccessor; -class CacheFileWriterRunnable implements Runnable { - String id; - String lockFilePath; - File file; - CrossProcessCacheFileLock lock; +class CacheFileWriterRunnable extends CacheWriterRunnable { - CacheFileWriterRunnable(String id, String lockFilePath, String filePath) { - this.id = id; - this.lockFilePath = lockFilePath; - this.file = new File(filePath); + CacheFileWriterRunnable(String id, String lockFilePath, String filePath, String lockHoldingIntervalsFilePath) { + this.lockHoldingIntervalsFilePath = lockHoldingIntervalsFilePath; - lock = new CrossProcessCacheFileLock(lockFilePath, 100, 10); - } - - public void run() { - try { - lock.writeLock(); - file.createNewFile(); - try (FileOutputStream os = new FileOutputStream(file, true)) { - os.write(("< " + id + "\n").getBytes()); - Thread.sleep(1000); - os.write(("> " + id + "\n").getBytes()); - } - } catch (IOException | InterruptedException ex) { - System.out.println("File write failure"); - ex.printStackTrace(); - } finally { - try { - lock.unlock(); - } catch (IOException e) { - System.out.println("Failed to unlock"); - e.printStackTrace(); - } - } + lock = new CrossProcessCacheFileLock(lockFilePath, 150, 100); + cacheAccessor = new CacheFileAccessor(filePath); } } \ No newline at end of file diff --git a/src/test/java/com/microsoft/aad/msal4jextensions/CacheLockFileStorageTest.java b/src/test/java/com/microsoft/aad/msal4jextensions/CacheLockFileStorageTest.java new file mode 100644 index 0000000..7d1f81c --- /dev/null +++ b/src/test/java/com/microsoft/aad/msal4jextensions/CacheLockFileStorageTest.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4jextensions; + +import com.microsoft.aad.msal4jextensions.persistence.CacheFileAccessor; +import org.junit.Test; + +import java.io.IOException; + +public class CacheLockFileStorageTest extends CacheLockTestBase { + + CacheFileAccessor cacheFileAccessor = new CacheFileAccessor(testFilePath); + + class CacheFileWriterRunnableFactory implements IRunnableFactory { + @Override + public Runnable create(String id) { + return new CacheFileWriterRunnable + (id, lockFilePath, testFilePath, lockHoldingIntervalsFilePath); + } + } + + @Test + public void multipleThreadsWriting_CacheFile() throws IOException, InterruptedException { + int numOfThreads = 100; + + multipleThreadsWriting(cacheFileAccessor, numOfThreads, new CacheFileWriterRunnableFactory()); + } + + @Test + public void multipleProcessesWriting_CacheFile() throws IOException, InterruptedException { + int numOfProcesses = 20; + + String writerClass = com.microsoft.aad.msal4jextensions.CacheFileWriter.class.getName(); + + String writerClassArgs = lockFilePath + " " + + testFilePath + " " + + lockHoldingIntervalsFilePath; + + multipleProcessesWriting(cacheFileAccessor, numOfProcesses, writerClass, writerClassArgs); + } +} diff --git a/src/test/java/com/microsoft/aad/msal4jextensions/CacheLockKeyChainStorageTest.java b/src/test/java/com/microsoft/aad/msal4jextensions/CacheLockKeyChainStorageTest.java new file mode 100644 index 0000000..30c9aa8 --- /dev/null +++ b/src/test/java/com/microsoft/aad/msal4jextensions/CacheLockKeyChainStorageTest.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4jextensions; + +import com.microsoft.aad.msal4jextensions.persistence.mac.KeyChainAccessor; +import com.sun.jna.Platform; +import org.junit.*; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; +import java.util.stream.Collectors; + +public class CacheLockKeyChainStorageTest extends CacheLockTestBase { + + public static final String SERVICE = "testMsalService"; + public static final String ACCOUNT = "testMsalAccount"; + + KeyChainAccessor keyChainAccessor = + new KeyChainAccessor(testFilePath, SERVICE, ACCOUNT); + + class KeyChainWriterRunnableFactory implements IRunnableFactory { + @Override + public Runnable create(String id) { + return new KeyChainWriterRunnable + (id, lockFilePath, testFilePath, lockHoldingIntervalsFilePath, + SERVICE, + ACCOUNT); + } + } + + @Test + public void multipleThreadsWriting_KeyChain() throws IOException, InterruptedException { + int numOfThreads = 100; + + multipleThreadsWriting(keyChainAccessor, numOfThreads, new KeyChainWriterRunnableFactory()); + } + + + @Test + public void multipleProcessesWriting_KeyChain() throws IOException, InterruptedException { + int numOfProcesses = 10; + + String writerClass = com.microsoft.aad.msal4jextensions.KeyChainWriter.class.getName(); + + String writerClassArgs = lockFilePath + " " + + testFilePath + " " + + lockHoldingIntervalsFilePath + " " + + SERVICE + " " + + ACCOUNT; + + multipleProcessesWriting(keyChainAccessor, numOfProcesses, writerClass, writerClassArgs); + } +} diff --git a/src/test/java/com/microsoft/aad/msal4jextensions/CacheLockKeyRingStorageTest.java b/src/test/java/com/microsoft/aad/msal4jextensions/CacheLockKeyRingStorageTest.java new file mode 100644 index 0000000..925b5ed --- /dev/null +++ b/src/test/java/com/microsoft/aad/msal4jextensions/CacheLockKeyRingStorageTest.java @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4jextensions; + +import com.microsoft.aad.msal4jextensions.persistence.linux.KeyRingAccessor; +import org.junit.Test; + +import java.io.IOException; + +public class CacheLockKeyRingStorageTest extends CacheLockTestBase { + + String SCHEMA = "testMsalSchema"; + String LABEL = "testMsalLabel"; + String ATTRIBUTE1_KEY = "testMsalAttribute1Key"; + String ATTRIBUTE1_VALUE = "testMsalAttribute1Value"; + String ATTRIBUTE2_KEY = "testMsalAttribute2Key"; + String ATTRIBUTE2_VALUE = "testMsalAttribute2Value"; + + KeyRingAccessor keyRingAccessor = new KeyRingAccessor(testFilePath, + null, + SCHEMA, + LABEL, + ATTRIBUTE1_KEY, + ATTRIBUTE1_VALUE, + ATTRIBUTE2_KEY, + ATTRIBUTE2_VALUE); + + class KeyRingWriterRunnableFactory implements IRunnableFactory { + @Override + public Runnable create(String id) { + return new KeyRingWriterRunnable + (id, lockFilePath, testFilePath, lockHoldingIntervalsFilePath, + SCHEMA, + LABEL, + ATTRIBUTE1_KEY, + ATTRIBUTE1_VALUE, + ATTRIBUTE2_KEY, + ATTRIBUTE2_VALUE); + } + } + + @Test + public void multipleThreadsWriting_KeyRing() throws IOException, InterruptedException { + int numOfThreads = 100; + + multipleThreadsWriting(keyRingAccessor, numOfThreads, new KeyRingWriterRunnableFactory()); + } + + @Test + public void multipleProcessesWriting_KeyRing() throws IOException, InterruptedException { + int numOfProcesses = 20; + + String writerClass = com.microsoft.aad.msal4jextensions.KeyRingWriter.class.getName(); + + String writerClassArgs = lockFilePath + " " + + testFilePath + " " + + lockHoldingIntervalsFilePath+ " " + + SCHEMA + " " + + LABEL + " " + + ATTRIBUTE1_KEY + " " + + ATTRIBUTE1_VALUE + " " + + ATTRIBUTE2_KEY + " " + + ATTRIBUTE2_VALUE; + + multipleProcessesWriting(keyRingAccessor, numOfProcesses, writerClass, writerClassArgs); + } +} diff --git a/src/test/java/com/microsoft/aad/msal4jextensions/CacheLockTest.java b/src/test/java/com/microsoft/aad/msal4jextensions/CacheLockTest.java deleted file mode 100644 index 8506a4c..0000000 --- a/src/test/java/com/microsoft/aad/msal4jextensions/CacheLockTest.java +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.microsoft.aad.msal4jextensions; - -import com.sun.jna.Platform; -import org.junit.*; - -import java.io.*; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -public class CacheLockTest { - - private static String folder; - private static String testFilePath; - private static String lockFilePath; - - @BeforeClass - public static void setup() { - // get proper file paths - String currDir = System.getProperty("user.dir"); - String home = System.getProperty("user.home"); - - java.nio.file.Path classes = java.nio.file.Paths.get(currDir, "target", "classes"); - java.nio.file.Path tests = java.nio.file.Paths.get(currDir, "target", "test-classes"); - - testFilePath = java.nio.file.Paths.get(home, "test.txt").toString(); - lockFilePath = java.nio.file.Paths.get(home, "testLock.lockfile").toString(); - - String delimiter = ":"; - if (Platform.isWindows()) { - delimiter = ";"; - } - folder = classes.toString() + delimiter + tests; - } - - @Test - public void tenThreadsWritingToFile_notSharedLock() throws IOException, InterruptedException { - int NUM_OF_THREADS = 10; - - File tester = new File(testFilePath); - tester.delete(); - - List writersThreads = new ArrayList<>(); - for (int i = 0; i < NUM_OF_THREADS; i++) { - CacheFileWriterRunnable cacheFileWriterRunnable = - new CacheFileWriterRunnable("Thread # " + i, lockFilePath, testFilePath); - - writersThreads.add(new Thread(cacheFileWriterRunnable)); - } - - for (Thread t : writersThreads) { - t.start(); - t.join(); - } - - validateResult(); - } - - - @Test - public void tenThreadsWritingToFile_sharedLock() throws IOException, InterruptedException { - int NUM_OF_THREADS = 10; - - File tester = new File(testFilePath); - tester.delete(); - - List writersThreads = new ArrayList<>(); - CacheFileWriterRunnable cacheFileWriterRunnable = - new CacheFileWriterRunnable("Thread # ", lockFilePath, testFilePath); - - for (int i = 0; i < NUM_OF_THREADS; i++) { - writersThreads.add(new Thread(cacheFileWriterRunnable)); - } - - for (Thread t : writersThreads) { - t.start(); - t.join(); - } - - validateResult(); - } - - // implementation of org/slf4j/LoggerFactory should be available in Path - //@Test - public void tenProcessesWritingToFile() throws IOException, InterruptedException { - int NUM_OF_PROCESSES = 10; - // make sure tester.json file doesn't already exist - File tester = new File(testFilePath); - tester.delete(); - - String mainWriterClass = com.microsoft.aad.msal4jextensions.CacheFileWriter.class.getName(); - - List processes = new ArrayList<>(); - for (int i = 0; i < NUM_OF_PROCESSES; i++) { - String[] command = - new String[]{"java", "-cp", folder, mainWriterClass, "Process # " + i, lockFilePath, testFilePath}; - - Process process = new ProcessBuilder(command).inheritIO().start(); - processes.add(process); - } - - for (Process process : processes) { - waitForProcess(process); - } - - validateResult(); - } - - private void validateResult() throws IOException { - String prevTag = null; - String prevProcId = null; - - try (BufferedReader br = new BufferedReader(new FileReader(new File(testFilePath)))) { - String line; - while ((line = br.readLine()) != null) { - String[] tokens = line.split(" "); - String tag = tokens[0]; - String procId = tokens[1]; - switch (tag) { - case ("<"): - if ("<".equals(prevTag)) { - Assert.fail("Unexpected Token"); - } - break; - case (">"): - if (!"<".equals(prevTag) || !prevProcId.equals(procId)) { - Assert.fail("Unexpected Token"); - } - break; - default: - Assert.fail("Unexpected Token"); - } - prevTag = tag; - prevProcId = procId; - } - } - if (!">".equals(prevTag)) { - Assert.fail("Unexpected Token"); - } - } - - private void waitForProcess(Process process) throws InterruptedException { - if (process.waitFor() != 0) { - throw new RuntimeException(new BufferedReader(new InputStreamReader(process.getErrorStream())) - .lines().collect(Collectors.joining("\n"))); - } - } -} diff --git a/src/test/java/com/microsoft/aad/msal4jextensions/CacheLockTestBase.java b/src/test/java/com/microsoft/aad/msal4jextensions/CacheLockTestBase.java new file mode 100644 index 0000000..93ef9e6 --- /dev/null +++ b/src/test/java/com/microsoft/aad/msal4jextensions/CacheLockTestBase.java @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4jextensions; + +import com.microsoft.aad.msal4jextensions.persistence.ICacheAccessor; +import com.sun.jna.Platform; +import org.junit.Assert; +import org.junit.BeforeClass; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +public class CacheLockTestBase { + static String folder; + static String testFilePath; + static String lockFilePath; + + static String testClassesPath; + + static String lockHoldingIntervalsFilePath; + + @BeforeClass + public static void setup() { + // get proper file paths + String currDir = System.getProperty("user.dir"); + String home = System.getProperty("user.home"); + + java.nio.file.Path classes = java.nio.file.Paths.get(currDir, "target", "classes"); + testClassesPath = java.nio.file.Paths.get(currDir, "target", "test-classes").toString(); + + testFilePath = java.nio.file.Paths.get(home, "test.txt").toString(); + lockFilePath = java.nio.file.Paths.get(home, "testLock.lockfile").toString(); + + lockHoldingIntervalsFilePath = java.nio.file.Paths.get(home, "lockHoldingIntervals.txt").toString(); + + String delimiter = ":"; + if (Platform.isWindows()) { + delimiter = ";"; + } + folder = classes.toString() + delimiter + testClassesPath; + } + + void waitForProcess(Process process) throws InterruptedException { + if (process.waitFor() != 0) { + throw new RuntimeException(new BufferedReader(new InputStreamReader(process.getErrorStream())) + .lines().collect(Collectors.joining("\n"))); + } + } + + void validateResult(String data, int expectedNum) { + System.out.println("DATA TO VALIDATE: "); + System.out.println(data); + + String prevTag = null; + String prevProcId = null; + int count = 0; + + for (String line : data.split("\\r?\\n")) { + + String[] tokens = line.split(" "); + String tag = tokens[0]; + String procId = tokens[1]; + switch (tag) { + case ("<"): + if ("<".equals(prevTag)) { + Assert.fail("Unexpected Token"); + } + break; + case (">"): + count++; + if (!"<".equals(prevTag) || !prevProcId.equals(procId)) { + Assert.fail("Unexpected Token"); + } + break; + default: + Assert.fail("Unexpected Token"); + } + prevTag = tag; + prevProcId = procId; + } + if (!">".equals(prevTag)) { + Assert.fail("Unexpected Token"); + } + Assert.assertEquals(expectedNum, count); + } + + void validateLockUsageIntervals(int expected_size) throws IOException { + List list = new ArrayList<>(); + String data = readFile(lockHoldingIntervalsFilePath); + + for (String line : data.split("\\r?\\n")) { + String[] split = line.split("-"); + list.add(new Long[]{Long.parseLong(split[0]), Long.parseLong(split[1])}); + } + + //Assert.assertEquals(expected_size, list.size()); + if (expected_size != list.size()) { + System.out.println("lock intervals NUM = " + list.size()); + } + + list.sort(Comparator.comparingLong(a -> a[0])); + + int sum = 0; + Long[] prev = null; + for (Long[] interval : list) { + Assert.assertTrue(interval[0] <= interval[1]); + sum += interval[1] - interval[0]; + if (prev != null) { + if (interval[0] < prev[1]) { + System.out.println("lock acquisition intersection detected"); + //Assert.fail(); + } + } + prev = interval; + } + System.out.println("average lock holding time in ms - " + sum/list.size()); + } + + private String readFile(String filePath) throws IOException { + byte[] bytes = Files.readAllBytes(Paths.get(filePath)); + return new String(bytes, StandardCharsets.UTF_8); + } + + void validateResultInCache(ICacheAccessor keyChainAccessor, int expectedNum) throws IOException { + validateResult(new String(keyChainAccessor.read(), StandardCharsets.UTF_8), expectedNum); + } + + void multipleThreadsWriting(ICacheAccessor cacheAccessor, int num, + IRunnableFactory runnableFactory) throws IOException, InterruptedException { + + clearFile(lockHoldingIntervalsFilePath); + cacheAccessor.delete(); + + List writersThreads = new ArrayList<>(); + + for (int i = 0; i < num; i++) { + + Thread t = new Thread(runnableFactory.create("thread_" + i)); + t.start(); + writersThreads.add(t); + } + + for (Thread t : writersThreads) { + t.join(); + } + validateLockUsageIntervals(num); + validateResultInCache(cacheAccessor, num); + } + + private void clearFile(String filePath) throws IOException { + new FileOutputStream(filePath).close(); + } + + void multipleProcessesWriting(ICacheAccessor cacheAccessor, int num, + String writerClass, + String writerClassArgs) + throws IOException, InterruptedException { + + clearFile(lockHoldingIntervalsFilePath); + cacheAccessor.delete(); + + List processes = new ArrayList<>(); + for (int i = 0; i < num; i++) { + + String mvnArgs = ("Process_" + i) + " " + writerClassArgs; + + String mvn = Platform.isWindows() ? "mvn.bat" : "mvn"; + + String[] mvnCommand = + new String[]{mvn, "exec:java", + "-Dexec.mainClass=" + writerClass, + "-Dexec.classpathScope=test", + "-Dexec.args=" + mvnArgs}; + + Process process = new ProcessBuilder(mvnCommand).inheritIO().start(); + processes.add(process); + } + + for (Process process : processes) { + waitForProcess(process); + } + + validateLockUsageIntervals(num); + validateResultInCache(cacheAccessor, num); + } +} diff --git a/src/test/java/com/microsoft/aad/msal4jextensions/CacheWriterRunnable.java b/src/test/java/com/microsoft/aad/msal4jextensions/CacheWriterRunnable.java new file mode 100644 index 0000000..95a7599 --- /dev/null +++ b/src/test/java/com/microsoft/aad/msal4jextensions/CacheWriterRunnable.java @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4jextensions; + +import com.microsoft.aad.msal4jextensions.persistence.ICacheAccessor; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +public class CacheWriterRunnable implements Runnable { + ICacheAccessor cacheAccessor; + + CrossProcessCacheFileLock lock; + + String lockHoldingIntervalsFilePath; + + @Override + public void run() { + long start = 0; + long end = 0; + + try { + lock.lock(); + start = System.currentTimeMillis(); + + String jvmName = java.lang.management.ManagementFactory.getRuntimeMXBean().getName(); + String id = jvmName + ":" + Thread.currentThread().getId(); + + byte[] data = cacheAccessor.read(); + + String strData = (data != null) ? new String(data, StandardCharsets.UTF_8) : ""; + strData += "< " + id + "\n"; + strData += "> " + id + "\n"; + + // in average deserialize/serialize of token cache with 100 tokens takes 130 ms + Thread.sleep(150); + + cacheAccessor.write(strData.getBytes(StandardCharsets.UTF_8)); + end = System.currentTimeMillis(); + } catch (Exception ex) { + System.out.println("File write failure " + ex.getMessage()); + ex.printStackTrace(); + } finally { + try { + lock.unlock(); + if(start > 0 && end > 0) { + try (FileOutputStream os = new FileOutputStream(lockHoldingIntervalsFilePath, true)) { + os.write((start + "-" + end + "\n").getBytes()); + } + } + } catch (IOException e) { + System.out.println("Failed to unlock"); + e.printStackTrace(); + } + } + } +} diff --git a/src/test/java/com/microsoft/aad/msal4jextensions/IRunnableFactory.java b/src/test/java/com/microsoft/aad/msal4jextensions/IRunnableFactory.java new file mode 100644 index 0000000..4d82ae8 --- /dev/null +++ b/src/test/java/com/microsoft/aad/msal4jextensions/IRunnableFactory.java @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4jextensions; + +public interface IRunnableFactory { + Runnable create(String id); +} diff --git a/src/test/java/com/microsoft/aad/msal4jextensions/IntegrationTest.java b/src/test/java/com/microsoft/aad/msal4jextensions/IntegrationTest.java index d673350..a1d2fff 100644 --- a/src/test/java/com/microsoft/aad/msal4jextensions/IntegrationTest.java +++ b/src/test/java/com/microsoft/aad/msal4jextensions/IntegrationTest.java @@ -1,9 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + package com.microsoft.aad.msal4jextensions; import com.microsoft.aad.msal4j.*; import org.junit.Assert; -import org.junit.Test; - import java.io.IOException; import java.net.MalformedURLException; import java.nio.file.Path; @@ -41,9 +42,9 @@ private ConfidentialClientApplication createConfidentialClient() throws IOExcept ClientCredentialFactory.createFromSecret(TestData.CONFIDENTIAL_CLIENT_SECRET); return ConfidentialClientApplication.builder(TestData.CONFIDENTIAL_CLIENT_ID, clientCredential) - .authority(TestData.TENANT_SPECIFIC_AUTHORITY) - .setTokenCacheAccessAspect(createPersistenceAspect()) - .build(); + .authority(TestData.TENANT_SPECIFIC_AUTHORITY) + .setTokenCacheAccessAspect(createPersistenceAspect()) + .build(); } // @Test @@ -81,4 +82,4 @@ private IAuthenticationResult acquireTokenSilently(ConfidentialClientApplication return future.join(); } -} +} \ No newline at end of file diff --git a/src/test/java/com/microsoft/aad/msal4jextensions/KeyChainWriter.java b/src/test/java/com/microsoft/aad/msal4jextensions/KeyChainWriter.java new file mode 100644 index 0000000..58e20ed --- /dev/null +++ b/src/test/java/com/microsoft/aad/msal4jextensions/KeyChainWriter.java @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4jextensions; + +public class KeyChainWriter { + + public static void main(String[] args) throws Exception { + String executionId = args[0]; + String lockFilePath = args[1]; + String filePath = args[2]; + String lockHoldingIntervalsFilePath = args[3]; + + String serviceName = args[4]; + String accountName = args[5]; + + try { + KeyChainWriterRunnable keyChainWriterRunnable = + new KeyChainWriterRunnable(executionId, lockFilePath, filePath, lockHoldingIntervalsFilePath, + serviceName, accountName); + + keyChainWriterRunnable.run(); + System.out.println("executionId - " + executionId + " SUCCESS"); + } + catch (Throwable e){ + System.out.println("executionId - " + executionId + ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> FAILURE <<<<<<<<<<<<<<<<<<<<<<<<<"); + System.out.println(e.getMessage()); + } + } +} diff --git a/src/test/java/com/microsoft/aad/msal4jextensions/KeyChainWriterRunnable.java b/src/test/java/com/microsoft/aad/msal4jextensions/KeyChainWriterRunnable.java new file mode 100644 index 0000000..8ccb8af --- /dev/null +++ b/src/test/java/com/microsoft/aad/msal4jextensions/KeyChainWriterRunnable.java @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4jextensions; + +import com.microsoft.aad.msal4jextensions.persistence.mac.KeyChainAccessor; + +public class KeyChainWriterRunnable extends CacheWriterRunnable { + + KeyChainWriterRunnable + (String id, String lockFilePath, String filePath, String lockHoldingIntervalsFilePath, + String serviceName, String accountName) { + + this.lockHoldingIntervalsFilePath = lockHoldingIntervalsFilePath; + + lock = new CrossProcessCacheFileLock(lockFilePath, 150, 100); + + cacheAccessor = new KeyChainAccessor(filePath, serviceName, accountName); + } +} diff --git a/src/test/java/com/microsoft/aad/msal4jextensions/KeyRingWriter.java b/src/test/java/com/microsoft/aad/msal4jextensions/KeyRingWriter.java new file mode 100644 index 0000000..daaed96 --- /dev/null +++ b/src/test/java/com/microsoft/aad/msal4jextensions/KeyRingWriter.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4jextensions; + +public class KeyRingWriter { + + public static void main(String[] args) throws Exception { + + String executionId = args[0]; + String lockFilePath = args[1]; + String filePath = args[2]; + String lockHoldingIntervalsFilePath = args[3]; + + String schema = args[4]; + String label = args[5]; + String attribute1Key = args[6]; + String attribute1Value = args[7]; + String attribute2Key = args[8]; + String attribute2Value = args[9]; + + try { + KeyRingWriterRunnable keyRingWriterRunnable = + new KeyRingWriterRunnable(executionId, + lockFilePath, filePath, lockHoldingIntervalsFilePath, + schema, + label, + attribute1Key, + attribute1Value, + attribute2Key, + attribute2Value); + + keyRingWriterRunnable.run(); + } + catch (Throwable e){ + System.out.println("executionId - " + executionId + ">>>>>>>>>>>>>>>>> KeyRingWriter FAILURE <<<<<<<<<<<<<<<<<<<<<<<<<"); + System.out.println(e.getMessage()); + } + } +} diff --git a/src/test/java/com/microsoft/aad/msal4jextensions/KeyRingWriterRunnable.java b/src/test/java/com/microsoft/aad/msal4jextensions/KeyRingWriterRunnable.java new file mode 100644 index 0000000..93cf0ff --- /dev/null +++ b/src/test/java/com/microsoft/aad/msal4jextensions/KeyRingWriterRunnable.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.microsoft.aad.msal4jextensions; + +import com.microsoft.aad.msal4jextensions.persistence.linux.KeyRingAccessor; + +public class KeyRingWriterRunnable extends CacheWriterRunnable { + + KeyRingWriterRunnable + (String id, + String lockFilePath, String filePath, String lockHoldingIntervalsFilePath, + String schema, + String label, + String attribute1Key, + String attribute1Value, + String attribute2Key, + String attribute2Value) { + + this.lockHoldingIntervalsFilePath = lockHoldingIntervalsFilePath; + + lock = new CrossProcessCacheFileLock(lockFilePath, 150, 100); + + cacheAccessor = new KeyRingAccessor(filePath, + null, + schema, + label, + attribute1Key, + attribute1Value, + attribute2Key, + attribute2Value); + } +} diff --git a/src/test/java/com/microsoft/aad/msal4jextensions/TestData.java b/src/test/java/com/microsoft/aad/msal4jextensions/TestData.java index ed3b052..20781cc 100644 --- a/src/test/java/com/microsoft/aad/msal4jextensions/TestData.java +++ b/src/test/java/com/microsoft/aad/msal4jextensions/TestData.java @@ -2,10 +2,11 @@ public class TestData { - static String TENANT_SPECIFIC_AUTHORITY = "https://login.microsoftonline.com/"; + static String TENANT_SPECIFIC_AUTHORITY = "https://login.microsoftonline.com/pesomka.onmicrosoft.com/"; + static String AUTHORITY_ORGANIZATION = "https://login.microsoftonline.com/organizations/"; static String GRAPH_DEFAULT_SCOPE = "https://graph.windows.net/.default"; - static String CONFIDENTIAL_CLIENT_ID = ""; - static String CONFIDENTIAL_CLIENT_SECRET = ""; + static String CONFIDENTIAL_CLIENT_ID = ""; + static String CONFIDENTIAL_CLIENT_SECRET = ""; } diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties new file mode 100644 index 0000000..73631c6 --- /dev/null +++ b/src/test/resources/log4j.properties @@ -0,0 +1,5 @@ +log4j.rootLogger=TRACE, stdout +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd'T'HH:mm:ss.SSS} %-5p [%c] - %m%n \ No newline at end of file