# HG changeset patch # User Vladimir Kvashin # Date 1479453781 -10800 # Branch release82 # Node ID 71a2bd4df67b92af5bc67b270182dc67a6def6c1 # Parent 13ee6f16f094717f3fa9f450d6d9ca01a46e8c86 Fixing #268926 (Remote File System should support DeleteOnExit) diff -r 13ee6f16f094 -r 71a2bd4df67b dlight.remote.impl/src/org/netbeans/modules/remote/impl/fs/RemoteFileSystem.java --- a/dlight.remote.impl/src/org/netbeans/modules/remote/impl/fs/RemoteFileSystem.java Wed Nov 16 13:27:01 2016 +0300 +++ b/dlight.remote.impl/src/org/netbeans/modules/remote/impl/fs/RemoteFileSystem.java Fri Nov 18 10:23:01 2016 +0300 @@ -45,16 +45,22 @@ import java.awt.Image; import java.awt.event.WindowEvent; import java.awt.event.WindowFocusListener; +import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; +import java.io.FileReader; import java.io.IOException; import java.io.InterruptedIOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.UnsupportedEncodingException; import java.lang.ref.WeakReference; import java.net.ConnectException; import java.util.*; @@ -70,12 +76,14 @@ import org.netbeans.modules.dlight.libs.common.PathUtilities; import org.netbeans.modules.nativeexecution.api.ExecutionEnvironment; import org.netbeans.modules.nativeexecution.api.HostInfo; +import org.netbeans.modules.nativeexecution.api.NativeProcessBuilder; import org.netbeans.modules.nativeexecution.api.util.CommonTasksSupport; import org.netbeans.modules.nativeexecution.api.util.ConnectionListener; import org.netbeans.modules.nativeexecution.api.util.ConnectionManager; import org.netbeans.modules.nativeexecution.api.util.ConnectionManager.CancellationException; import org.netbeans.modules.nativeexecution.api.util.FileInfoProvider; import org.netbeans.modules.nativeexecution.api.util.HostInfoUtils; +import org.netbeans.modules.nativeexecution.api.util.ProcessUtils; import org.netbeans.modules.remote.actions.FastPasteAction; import org.netbeans.modules.remote.api.ConnectionNotifier; import org.netbeans.modules.remote.impl.RemoteLogger; @@ -140,7 +148,19 @@ private final List problemListeners = new ArrayList<>(globalProblemListeners); transient private final StatusImpl status = new StatusImpl(); + + /** If the ALLOW_ALTERNATIVE_DELETE_ON_EXIT is ON and transport does not support delete-on-exit, + * then alternative delete-on-exit will work */ + public static final boolean ALLOW_ALTERNATIVE_DELETE_ON_EXIT = + RemoteFileSystemUtils.getBoolean("remote.alternative.delete.on.exit", true); + public static final String DELETE_ON_EXIT_FILE_NAME = ".rfs_delete_on_exit"; // NOI18N + /** guarded by self */ private final LinkedHashSet deleteOnExitFiles = new LinkedHashSet<>(); + /** + * guarded by deleteOnExitFiles + * The idea is to remove files only on 1-st connect; otherwise we'll get to complex sync */ + boolean deleteOnExitFlag; + private final ThreadLocal beingRemoved = new ThreadLocal<>(); private final ThreadLocal beingCreated = new ThreadLocal<>(); private final ThreadLocal externallyRemoved = new ThreadLocal<>(); @@ -186,6 +206,12 @@ if (!cache.exists() && !cache.mkdirs()) { throw new IOException(NbBundle.getMessage(getClass(), "ERR_CreateDir", cache.getAbsolutePath())); // new IOException sic! (ctor) } + if (ALLOW_ALTERNATIVE_DELETE_ON_EXIT) { + loadDeleteOnExit(); + synchronized (deleteOnExitFiles) { + deleteOnExitFlag = true; + } + } this.rootDelegate = new RootFileObject(this.root = new RemoteFileObject(this), this, execEnv, cache); // NOI18N factory.register(rootDelegate); @@ -347,6 +373,11 @@ fo.connectionChanged(); } } + if (ALLOW_ALTERNATIVE_DELETE_ON_EXIT) { + if (ConnectionManager.getInstance().isConnectedTo(execEnv)) { + deleteOnExitImpl(); + } + } } private void maintainAutoMounts() { @@ -420,6 +451,10 @@ if (COLLECT_STATSISTICS) { lockSupport.printStatistics(this); } + if (ALLOW_ALTERNATIVE_DELETE_ON_EXIT) { + // right here: on IDE shutdown we otherwise won't be in time to store + storeDeleteOnExit(); + } } public ExecutionEnvironment getExecutionEnvironment() { @@ -949,36 +984,83 @@ return status; } - public void deleteOnExit(String path) { + public void deleteOnExit(String... paths) { synchronized(deleteOnExitFiles) { - if (deleteOnExitFiles.isEmpty()) { - Runtime.getRuntime().addShutdownHook(new Thread() { - - @Override - public void run() { - releaseResources(); - } - - }); + for (String p : paths) { + deleteOnExitFiles.add(p); } - deleteOnExitFiles.add(path); } } - private void releaseResources() { - ArrayList toBeDeleted; + private void deleteOnExitImpl() { + assert ALLOW_ALTERNATIVE_DELETE_ON_EXIT; + List toBeDeleted = Collections.emptyList(); synchronized(deleteOnExitFiles) { - toBeDeleted = new ArrayList<>(deleteOnExitFiles); + if (deleteOnExitFlag && !deleteOnExitFiles.isEmpty()) { + deleteOnExitFlag = false; // delete only on 1-st connect, otherwize we get sync issues + toBeDeleted = new ArrayList<>(deleteOnExitFiles); + deleteOnExitFiles.clear(); + } + } + if (toBeDeleted.isEmpty()) { + return; } Collections.reverse(toBeDeleted); - for (String filename : toBeDeleted) { - if (!ConnectionManager.getInstance().isConnectedTo(execEnv)) { - return; + StringBuilder sb = new StringBuilder(); + for (String p : toBeDeleted) { + if (sb.length() > 0) { + sb.append(' '); } - CommonTasksSupport.rmFile(execEnv, filename, null); + sb.append(p); + } + if (!ConnectionManager.getInstance().isConnectedTo(execEnv)) { + return; + } + ProcessUtils.execute(NativeProcessBuilder.newProcessBuilder(execEnv).setExecutable("xargs").setArguments("rm"), sb.toString().getBytes()); + + } + + private void storeDeleteOnExit() { + assert ALLOW_ALTERNATIVE_DELETE_ON_EXIT; + ArrayList toStore; + synchronized (deleteOnExitFiles) { + toStore = new ArrayList<>(deleteOnExitFiles); + } + File f = new File(getCache(), DELETE_ON_EXIT_FILE_NAME); + // the existence of cache root ensured in ctor + try (PrintWriter pw = new PrintWriter(f, "UTF8")) { // NOI18N + if (!toStore.isEmpty()) { + for (String path : toStore) { + pw.append(path).append('\n'); + } + pw.close(); + } + } catch (FileNotFoundException | UnsupportedEncodingException ex) { + Exceptions.printStackTrace(ex); // should never occur } } + private void loadDeleteOnExit() { + assert ALLOW_ALTERNATIVE_DELETE_ON_EXIT; + File f = new File(getCache(), DELETE_ON_EXIT_FILE_NAME); + // the existence of cache root ensured in ctor + synchronized (deleteOnExitFiles) { + // this is called from ctor only, so it's OK to do file ops in sync block + try (BufferedReader br = new BufferedReader(new FileReader(f))) { + for (String path; (path = br.readLine()) != null;) { + if (!path.isEmpty()) { + deleteOnExitFiles.add(path); + } + } + // line is not visible here. + } catch (FileNotFoundException ex) { + // nothing to do: no file is quite normal + } catch (IOException ex) { + ex.printStackTrace(System.err); + } + } + } + /*package*/ void setBeingRemoved(RemoteFileObjectBase fo) { beingRemoved.set(fo); }