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