001/******************************************************************************* 002 * Copyright (c) 2006-2014 Nuxeo SA (http://nuxeo.com/) and others. 003 * 004 * All rights reserved. This program and the accompanying materials 005 * are made available under the terms of the Eclipse Public License v1.0 006 * which accompanies this distribution, and is available at 007 * http://www.eclipse.org/legal/epl-v10.html 008 ******************************************************************************/ 009package org.nuxeo.runtime.trackers.files; 010 011import java.io.File; 012import java.io.IOException; 013import java.util.HashSet; 014import java.util.Set; 015import java.util.concurrent.CopyOnWriteArrayList; 016 017import org.apache.commons.io.FileCleaningTracker; 018import org.apache.commons.io.FileDeleteStrategy; 019import org.apache.commons.logging.Log; 020import org.apache.commons.logging.LogFactory; 021import org.nuxeo.common.xmap.annotation.XObject; 022import org.nuxeo.runtime.RuntimeServiceEvent; 023import org.nuxeo.runtime.RuntimeServiceListener; 024import org.nuxeo.runtime.api.Framework; 025import org.nuxeo.runtime.model.ComponentContext; 026import org.nuxeo.runtime.model.ComponentInstance; 027import org.nuxeo.runtime.model.DefaultComponent; 028import org.nuxeo.runtime.services.event.EventService; 029import org.nuxeo.runtime.trackers.concurrent.ThreadEventHandler; 030import org.nuxeo.runtime.trackers.concurrent.ThreadEventListener; 031 032/** 033 * Files event tracker which delete files once the runtime leave the threads or at least once the associated marker 034 * object is garbaged. Note: for being backward compatible you may disable the thread events tracking by black-listing 035 * the default configuration component "org.nuxeo.runtime.trackers.files.threadstracking.config" in the runtime. This 036 * could be achieved by editing the "blacklist" file in your 'config' directory or using the @{link BlacklistComponent} 037 * annotation on your test class. 038 * 039 * @author Stephane Lacoin at Nuxeo (aka matic) 040 * @since 6.0 041 * @see ThreadEventHandler 042 */ 043public class FileEventTracker extends DefaultComponent { 044 045 protected static final Log log = LogFactory.getLog(FileEventTracker.class); 046 047 protected static SafeFileDeleteStrategy deleteStrategy = new SafeFileDeleteStrategy(); 048 049 static class SafeFileDeleteStrategy extends FileDeleteStrategy { 050 051 protected CopyOnWriteArrayList<String> protectedPaths = new CopyOnWriteArrayList<String>(); 052 053 protected SafeFileDeleteStrategy() { 054 super("DoNotTouchNuxeoBinaries"); 055 } 056 057 protected void registerProtectedPath(String path) { 058 protectedPaths.add(path); 059 } 060 061 protected boolean isFileProtected(File fileToDelete) { 062 for (String path : protectedPaths) { 063 // do not delete files under the protected directories 064 if (fileToDelete.getPath().startsWith(path)) { 065 log.warn("Protect file " + fileToDelete.getPath() 066 + " from deletion : check usage of Framework.trackFile"); 067 return true; 068 } 069 } 070 return false; 071 } 072 073 @Override 074 protected boolean doDelete(File fileToDelete) throws IOException { 075 if (!isFileProtected(fileToDelete)) { 076 return super.doDelete(fileToDelete); 077 } else { 078 return false; 079 } 080 } 081 082 } 083 084 /** 085 * Registers a protected path under which files should not be deleted 086 * 087 * @param path 088 * @since 7.2 089 */ 090 public static void registerProtectedPath(String path) { 091 deleteStrategy.registerProtectedPath(path); 092 } 093 094 protected class GCDelegate implements FileEventHandler { 095 protected FileCleaningTracker delegate = new FileCleaningTracker(); 096 097 @Override 098 public void onFile(File file, Object marker) { 099 delegate.track(file, marker, deleteStrategy); 100 } 101 } 102 103 protected class ThreadDelegate implements FileEventHandler { 104 105 protected final boolean isLongRunning; 106 107 protected final Thread owner = Thread.currentThread(); 108 109 protected final Set<File> files = new HashSet<File>(); 110 111 protected ThreadDelegate(boolean isLongRunning) { 112 this.isLongRunning = isLongRunning; 113 } 114 115 @Override 116 public void onFile(File file, Object marker) { 117 if (!owner.equals(Thread.currentThread())) { 118 return; 119 } 120 if (isLongRunning) { 121 gc.onFile(file, marker); 122 } 123 files.add(file); 124 } 125 126 } 127 128 @XObject("enableThreadsTracking") 129 public static class EnableThreadsTracking { 130 131 } 132 133 protected final GCDelegate gc = new GCDelegate(); 134 135 protected static FileEventTracker self; 136 137 protected final ThreadLocal<ThreadDelegate> threads = new ThreadLocal<>(); 138 139 protected final ThreadEventListener threadsListener = new ThreadEventListener(new ThreadEventHandler() { 140 141 @Override 142 public void onEnter(boolean isLongRunning) { 143 setThreadDelegate(isLongRunning); 144 } 145 146 @Override 147 public void onLeave() { 148 resetThreadDelegate(); 149 } 150 151 }); 152 153 protected final FileEventListener filesListener = new FileEventListener(new FileEventHandler() { 154 155 @Override 156 public void onFile(File file, Object marker) { 157 onContext().onFile(file, marker); 158 } 159 }); 160 161 @Override 162 public void activate(ComponentContext context) { 163 super.activate(context); 164 self = this; 165 filesListener.install(); 166 setThreadDelegate(false); 167 } 168 169 @Override 170 public int getApplicationStartedOrder() { 171 return Integer.MAX_VALUE; 172 } 173 174 @Override 175 public void applicationStarted(ComponentContext context) { 176 resetThreadDelegate(); 177 Framework.addListener(new RuntimeServiceListener() { 178 179 @Override 180 public void handleEvent(RuntimeServiceEvent event) { 181 if (event.id != RuntimeServiceEvent.RUNTIME_ABOUT_TO_STOP) { 182 return; 183 } 184 Framework.removeListener(this); 185 setThreadDelegate(false); 186 } 187 }); 188 } 189 190 @Override 191 public void deactivate(ComponentContext context) { 192 resetThreadDelegate(); 193 if (Framework.getService(EventService.class) != null) { 194 if (threadsListener.isInstalled()) { 195 threadsListener.uninstall(); 196 } 197 filesListener.uninstall(); 198 } 199 self = null; 200 super.deactivate(context); 201 } 202 203 @Override 204 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 205 if (contribution instanceof EnableThreadsTracking) { 206 threadsListener.install(); 207 } else { 208 super.registerContribution(contribution, extensionPoint, contributor); 209 } 210 211 } 212 213 protected FileEventHandler onContext() { 214 FileEventHandler actual = threads.get(); 215 if (actual == null) { 216 actual = gc; 217 } 218 return actual; 219 } 220 221 protected void setThreadDelegate(boolean isLongRunning) { 222 if (threads.get() != null) { 223 throw new IllegalStateException("Thread delegate already installed"); 224 } 225 threads.set(new ThreadDelegate(isLongRunning)); 226 } 227 228 protected void resetThreadDelegate() throws IllegalStateException { 229 ThreadDelegate actual = threads.get(); 230 if (actual == null) { 231 throw new IllegalStateException("Thread delegate not installed"); 232 } 233 try { 234 for (File file : actual.files) { 235 if (!deleteStrategy.isFileProtected(file)) { 236 file.delete(); 237 } 238 } 239 } finally { 240 threads.remove(); 241 } 242 } 243 244}