/* * Copyright 2000-2016 Vaadin Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.vaadin.server; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import javax.servlet.ServletConfig; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionBindingEvent; import org.easymock.EasyMock; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import com.vaadin.server.ClientConnector.DetachEvent; import com.vaadin.server.communication.UIInitHandler; import com.vaadin.ui.Label; import com.vaadin.ui.UI; import com.vaadin.util.CurrentInstance; public class VaadinSessionTest implements Serializable { private transient VaadinSession session; private transient VaadinServlet mockServlet; private transient VaadinServletService mockService; private transient ServletConfig mockServletConfig; private transient HttpSession mockHttpSession; private transient WrappedSession mockWrappedSession; private transient VaadinServletRequest vaadinRequest; private transient UI ui; private transient Lock httpSessionLock; @Before public void setup() throws Exception { httpSessionLock = new ReentrantLock(); mockServletConfig = new MockServletConfig(); mockServlet = new VaadinServlet(); mockServlet.init(mockServletConfig); mockService = mockServlet.getService(); mockHttpSession = EasyMock.createMock(HttpSession.class); mockWrappedSession = new WrappedHttpSession(mockHttpSession) { final ReentrantLock lock = new ReentrantLock(); { lock.lock(); } @Override public Object getAttribute(String name) { Object res; try { Thread.sleep(100); // for deadlock testing // simulates servlet container's session locking org.junit.Assert.assertTrue("Deadlock detected", httpSessionLock.tryLock(5, TimeUnit.SECONDS)); String lockAttribute = mockService.getServiceName() + ".lock"; if (lockAttribute.equals(name)) { res = lock; } else if ("com.vaadin.server.VaadinSession.Mock Servlet" .equals(name)) { res = session; } else { res = super.getAttribute(name); } httpSessionLock.unlock(); } catch (InterruptedException e) { throw new RuntimeException(e); } return res; } }; session = new VaadinSession(mockService); mockService.storeSession(session, mockWrappedSession); ui = new MockPageUI(); vaadinRequest = new VaadinServletRequest( EasyMock.createMock(HttpServletRequest.class), mockService) { @Override public String getParameter(String name) { if ("theme".equals(name) || "restartApplication".equals(name) || "ignoreRestart".equals(name) || "closeApplication".equals(name)) { return null; } else if (UIInitHandler.BROWSER_DETAILS_PARAMETER .equals(name)) { return "1"; } return super.getParameter(name); } @Override public String getMethod() { return "POST"; } @Override public WrappedSession getWrappedSession( boolean allowSessionCreation) { return mockWrappedSession; } }; ui.doInit(vaadinRequest, session.getNextUIid(), null); ui.setSession(session); session.addUI(ui); } /** * This reproduces #14452 situation with deadlock - see diagram */ @Test public void testInvalidationDeadlock() { // this simulates servlet container's session invalidation from another // thread new Thread(() -> { try { Thread.sleep(150); // delay selected so that VaadinSession // will be already locked by the main // thread // when we get here httpSessionLock.lock();// simulating servlet container's // session lock try { mockService.fireSessionDestroy(session); } finally { httpSessionLock.unlock(); } } catch (InterruptedException e) { throw new RuntimeException(e); } }).start(); try { mockService.findVaadinSession(vaadinRequest); } catch (Exception e) { throw new RuntimeException(e); } } @Test public void threadLocalsAfterUnderlyingSessionTimeout() throws InterruptedException { final AtomicBoolean detachCalled = new AtomicBoolean(false); ui.addDetachListener((DetachEvent event) -> { detachCalled.set(true); Assert.assertEquals(ui, UI.getCurrent()); Assert.assertEquals(ui.getPage(), Page.getCurrent()); Assert.assertEquals(session, VaadinSession.getCurrent()); Assert.assertEquals(mockService, VaadinService.getCurrent()); Assert.assertEquals(mockServlet, VaadinServlet.getCurrent()); }); session.valueUnbound( EasyMock.createMock(HttpSessionBindingEvent.class)); mockService.runPendingAccessTasks(session); // as soon as we changed // session.accessSynchronously // to session.access in // VaadinService.fireSessionDestroy, // we need to run the // pending task ourselves Assert.assertTrue(detachCalled.get()); } @Test public void threadLocalsAfterSessionDestroy() throws InterruptedException { final AtomicBoolean detachCalled = new AtomicBoolean(false); ui.addDetachListener((DetachEvent event) -> { detachCalled.set(true); Assert.assertEquals(ui, UI.getCurrent()); Assert.assertEquals(ui.getPage(), Page.getCurrent()); Assert.assertEquals(session, VaadinSession.getCurrent()); Assert.assertEquals(mockService, VaadinService.getCurrent()); Assert.assertEquals(mockServlet, VaadinServlet.getCurrent()); }); CurrentInstance.clearAll(); session.close(); mockService.cleanupSession(session); mockService.runPendingAccessTasks(session); // as soon as we changed // session.accessSynchronously // to session.access in // VaadinService.fireSessionDestroy, // we need to run the // pending task ourselves Assert.assertTrue(detachCalled.get()); } @Test public void testValueUnbound() { MockVaadinSession vaadinSession = new MockVaadinSession(mockService); vaadinSession.valueUnbound( EasyMock.createMock(HttpSessionBindingEvent.class)); org.junit.Assert.assertEquals( "'valueUnbound' method doesn't call 'close' for the session", 1, vaadinSession.getCloseCount()); vaadinSession.valueUnbound( EasyMock.createMock(HttpSessionBindingEvent.class)); org.junit.Assert.assertEquals( "'valueUnbound' method may not call 'close' " + "method for closing session", 1, vaadinSession.getCloseCount()); } // Can't define as an anonymous class since it would have a reference to // VaadinSessionTest.this which isn't serializable private static class MockPageUI extends UI { Page page = new Page(this, getState(false).pageState) { @Override public void init(VaadinRequest request) { } }; @Override protected void init(VaadinRequest request) { } @Override public Page getPage() { return page; } } private static class SerializationTestLabel extends Label { private transient VaadinSession session = VaadinSession.getCurrent(); private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); session = VaadinSession.getCurrent(); } } @Test public void threadLocalsWhenDeserializing() throws Exception { VaadinSession.setCurrent(session); session.lock(); SerializationTestLabel label = new SerializationTestLabel(); Assert.assertEquals("Session should be set when instance is created", session, label.session); ui.setContent(label); int uiId = ui.getUIId(); ByteArrayOutputStream bos = new ByteArrayOutputStream(); try (ObjectOutputStream out = new ObjectOutputStream(bos)) { out.writeObject(session); } session.unlock(); CurrentInstance.clearAll(); ObjectInputStream in = new ObjectInputStream( new ByteArrayInputStream(bos.toByteArray())); VaadinSession deserializedSession = (VaadinSession) in.readObject(); Assert.assertNull("Current session shouldn't leak from deserialisation", VaadinSession.getCurrent()); Assert.assertNotSame("Should get a new session", session, deserializedSession); // Restore http session and service instance so the session can be // locked deserializedSession.refreshTransients(mockWrappedSession, mockService); deserializedSession.lock(); UI deserializedUi = deserializedSession.getUIById(uiId); SerializationTestLabel deserializedLabel = (SerializationTestLabel) deserializedUi .getContent(); Assert.assertEquals( "Current session should be available in SerializationTestLabel.readObject", deserializedSession, deserializedLabel.session); deserializedSession.unlock(); } @Test public void lockedDuringSerialization() throws IOException { final AtomicBoolean lockChecked = new AtomicBoolean(false); ui.setContent(new Label() { private void writeObject(ObjectOutputStream out) throws IOException { Assert.assertTrue(session.hasLock()); lockChecked.set(true); out.defaultWriteObject(); } }); session.unlock(); Assert.assertFalse(session.hasLock()); ObjectOutputStream out = new ObjectOutputStream( new ByteArrayOutputStream()); out.writeObject(session); Assert.assertFalse(session.hasLock()); Assert.assertTrue(lockChecked.get()); } }