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