package org.objectstyle.wolips.goodies.core.mac; import static org.objectstyle.wolips.goodies.core.mac.jna.CoreServices.kFSEventStreamCreateFlagNoDefer; import java.io.IOException; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import org.eclipse.core.filesystem.URIUtil; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.refresh.IRefreshMonitor; import org.eclipse.core.resources.refresh.IRefreshResult; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.objectstyle.wolips.goodies.core.mac.jna.CoreFoundation; import org.objectstyle.wolips.goodies.core.mac.jna.CoreFoundation.CoreFoundationWrapper; import org.objectstyle.wolips.goodies.core.mac.jna.CoreServices; import org.objectstyle.wolips.goodies.core.mac.jna.CoreServices.CoreServicesWrapper; import org.objectstyle.wolips.goodies.core.mac.jna.CoreServices.FSEventStreamCallback; import com.sun.jna.NativeLibrary; import com.sun.jna.Pointer; public class MacRefreshMonitor extends Job implements IRefreshMonitor { final Map<IPath, MonitoredResource> _resources; CoreServices coreServices; CoreFoundation coreFoundation; long currentEvent = -1; private MonitorRefreshThread monitorThread; public MacRefreshMonitor() { super("MacRefreshMonitor"); setPriority(Job.DECORATE); setSystem(true); _resources = new ConcurrentHashMap<IPath, MonitoredResource>(); coreServices = CoreServicesWrapper.defaultInstance(); coreFoundation = CoreFoundationWrapper.defaultInstance(); currentEvent = coreServices.FSEventsGetCurrentEventId(); } @Override protected IStatus run(IProgressMonitor monitor) { closeStream(); monitorThread = new MonitorRefreshThread(); monitorThread.start(); return Status.OK_STATUS; } public void monitor(IResource resource, IRefreshResult refreshResult) { if (resource != null && refreshResult != null) { MonitoredResource monitoredResource = new MonitoredResource(resource, refreshResult); _resources.put(MacRefreshMonitor.canonicalPath(resource.getLocation()), monitoredResource); schedule(); } } public void unmonitor(IResource resource) { if (resource != null) { _resources.remove(resource.getLocation()); schedule(); } } protected static class MonitoredResource { public final IResource _resource; public final IRefreshResult _result; public MonitoredResource(IResource resource, IRefreshResult result) { _resource = resource; _result = result; } public IResource getResource() { return _resource; } public IRefreshResult getResult() { return _result; } protected void refresh(IResource resource) { // MS: SVN thrashes the fuck out of the refresher, and because it's always a // branch off of your main structure, it doesn't benefit from the refresh // coalescing that happens in RefreshManager. To help out a little bit, we // roll up team private members to their nearest non-team-private member so // it can be coalesced effectively. if (resource != null && resource.isTeamPrivateMember()) { IResource refreshResource = resource.getParent(); while (refreshResource != null && refreshResource.isTeamPrivateMember()) { refreshResource = refreshResource.getParent(); } if (refreshResource != null && !resource.isSynchronized(IResource.DEPTH_ONE)) { _result.refresh(refreshResource); } } else if (resource != null && !resource.isSynchronized(IResource.DEPTH_ONE)) { _result.refresh(resource); } } protected void pathChanged(IPath location) { IContainer matchingContainer = _resource.getWorkspace().getRoot().getContainerForLocation(location); if (matchingContainer != null) { refresh(matchingContainer); } else { IContainer[] matchingContainers = _resource.getWorkspace().getRoot().findContainersForLocationURI(URIUtil.toURI(location.makeAbsolute())); if (matchingContainers != null) { for (IContainer container : matchingContainers) { refresh(container); } } } } } public synchronized void dispose() { closeStream(); currentEvent = -1; coreFoundation = null; coreServices = null; } private void closeStream() { if (monitorThread != null) { monitorThread.cancel(); } } @Override protected void finalize() throws Throwable { dispose(); } public static IPath canonicalPath(IPath path) { if (path == null) return null; try { final String pathString = path.toOSString(); final String canonicalPath = new java.io.File(pathString).getCanonicalPath(); //only create a new path if necessary if (canonicalPath.equals(pathString)) return path; return new Path(canonicalPath); } catch (IOException e) { return path; } } MonitoredResource monitoredResourceForPath(IPath path) { IPath aPath = path; MonitoredResource resource = _resources.get(aPath); while (resource == null && !path.isRoot() && !"/".equals(aPath.toPortableString())) { aPath = aPath.removeLastSegments(1); resource = _resources.get(aPath); } return resource; } class StreamEventCallback implements FSEventStreamCallback { public void callback(Pointer streamRef, Pointer clientCallbackInfo, int numEvents, Pointer eventPaths, Pointer eventFlags, Pointer eventIds) { Pointer[] myPaths = eventPaths.getPointerArray(0, numEvents); long[] myEvents = eventIds.getLongArray(0, numEvents); for (int i = 0; i < numEvents; i++) { IPath path = MacRefreshMonitor.canonicalPath(new Path(myPaths[i].getString(0))); MonitoredResource resource = monitoredResourceForPath(path); if (resource != null) { resource.pathChanged(path); } if ((currentEvent > 0 && currentEvent < myEvents[i]) ||(currentEvent < 0 && currentEvent > myEvents[i])) { currentEvent = myEvents[i]; } } } } private class MonitorRefreshThread extends Thread { public MonitorRefreshThread() { super("MacRefreshMonitor"); } private Pointer runLoop; private Pointer streamRef; private StreamEventCallback callBack = new StreamEventCallback(); private CountDownLatch started = new CountDownLatch(1); @Override public void run() { Pointer runLoopMode = NativeLibrary.getInstance("CoreFoundation").getGlobalVariableAddress( "kCFRunLoopDefaultMode").getPointer(0); try { if (_resources.size() == 0) return; Pointer monitorPaths = coreFoundation.CFArrayCreate(resourcePaths()); streamRef = coreServices.FSEventStreamCreate(null, callBack, null, monitorPaths, currentEvent, 1.0, kFSEventStreamCreateFlagNoDefer); runLoop = coreFoundation.CFRunLoopGetCurrent(); coreServices.FSEventStreamScheduleWithRunLoop(streamRef, runLoop, runLoopMode); coreServices.FSEventStreamStart(streamRef); } finally { started.countDown(); } coreFoundation.CFRunLoopRun(); } private String[] resourcePaths() { List<String> result = new LinkedList<String>(); IPath[] paths = _resources.keySet().toArray(new IPath[_resources.size()]); for (int i = 0; i < paths.length; i++) { if (paths[i] != null) { result.add(paths[i].toOSString()); } } return result.toArray(new String[result.size()]); } synchronized void cancel() { try { started.await(); } catch (InterruptedException e) { e.printStackTrace(); } if (runLoop != null) { coreFoundation.CFRunLoopStop(runLoop); runLoop = null; } if (streamRef != null) { coreServices.FSEventStreamStop(streamRef); coreServices.FSEventStreamInvalidate(streamRef); coreServices.FSEventStreamRelease(streamRef); streamRef = null; } } } }