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 * @since 7.2 093 */ 094 public static void registerProtectedPath(String path) { 095 deleteStrategy.registerProtectedPath(path); 096 } 097 098 protected static class GCDelegate implements FileEventHandler { 099 protected FileCleaningTracker delegate = new FileCleaningTracker(); 100 101 @Override 102 public void onFile(File file, Object marker) { 103 delegate.track(file, marker, deleteStrategy); 104 } 105 } 106 107 protected class ThreadDelegate implements FileEventHandler { 108 109 protected final boolean isLongRunning; 110 111 protected final Thread owner = Thread.currentThread(); 112 113 protected final Set<File> files = new HashSet<>(); 114 115 protected ThreadDelegate(boolean isLongRunning) { 116 this.isLongRunning = isLongRunning; 117 } 118 119 @Override 120 public void onFile(File file, Object marker) { 121 if (!owner.equals(Thread.currentThread())) { 122 return; 123 } 124 if (isLongRunning) { 125 gc.onFile(file, marker); 126 } 127 files.add(file); 128 } 129 130 } 131 132 @XObject("enableThreadsTracking") 133 public static class EnableThreadsTracking { 134 135 } 136 137 protected final GCDelegate gc = new GCDelegate(); 138 139 protected final ThreadLocal<ThreadDelegate> threads = new ThreadLocal<>(); 140 141 protected final ThreadEventListener threadsListener = new ThreadEventListener(new ThreadEventHandler() { 142 143 @Override 144 public void onEnter(boolean isLongRunning) { 145 setThreadDelegate(isLongRunning); 146 } 147 148 @Override 149 public void onLeave() { 150 resetThreadDelegate(); 151 } 152 153 }); 154 155 protected final FileEventListener filesListener = new FileEventListener( 156 (file, marker) -> onContext().onFile(file, marker)); 157 158 @Override 159 public void activate(ComponentContext context) { 160 super.activate(context); 161 filesListener.install(); 162 setThreadDelegate(false); 163 } 164 165 @Override 166 public int getApplicationStartedOrder() { 167 return Integer.MAX_VALUE; 168 } 169 170 @Override 171 public void start(ComponentContext context) { 172 resetThreadDelegate(); 173 } 174 175 @Override 176 public void deactivate(ComponentContext context) { 177 if (Framework.getService(EventService.class) != null) { 178 if (threadsListener.isInstalled()) { 179 threadsListener.uninstall(); 180 } 181 filesListener.uninstall(); 182 } 183 super.deactivate(context); 184 } 185 186 @Override 187 public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) { 188 if (contribution instanceof EnableThreadsTracking) { 189 threadsListener.install(); 190 } else { 191 super.registerContribution(contribution, extensionPoint, contributor); 192 } 193 194 } 195 196 protected FileEventHandler onContext() { 197 FileEventHandler actual = threads.get(); 198 if (actual == null) { 199 actual = gc; 200 } 201 return actual; 202 } 203 204 protected void setThreadDelegate(boolean isLongRunning) { 205 if (threads.get() != null) { 206 throw new IllegalStateException("Thread delegate already installed"); 207 } 208 threads.set(new ThreadDelegate(isLongRunning)); 209 } 210 211 protected void resetThreadDelegate() throws IllegalStateException { 212 ThreadDelegate actual = threads.get(); 213 if (actual == null) { 214 return; 215 } 216 try { 217 for (File file : actual.files) { 218 if (!deleteStrategy.isFileProtected(file)) { 219 file.delete(); 220 } 221 } 222 } finally { 223 threads.remove(); 224 } 225 } 226 227}