jcifs.smb.SmbTreeConnection Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jcifs Show documentation
Show all versions of jcifs Show documentation
JCIFS is an Open Source client library that implements the CIFS/SMB networking protocol in 100% Java
/*
* © 2017 AgNO3 Gmbh & Co. KG
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package jcifs.smb;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Locale;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jcifs.CIFSContext;
import jcifs.CIFSException;
import jcifs.Configuration;
import jcifs.DfsReferralData;
import jcifs.RuntimeCIFSException;
import jcifs.SmbConstants;
import jcifs.SmbResourceLocator;
import jcifs.SmbTreeHandle;
import jcifs.internal.CommonServerMessageBlockRequest;
import jcifs.internal.CommonServerMessageBlockResponse;
import jcifs.internal.RequestWithPath;
import jcifs.internal.dfs.DfsReferralDataInternal;
import jcifs.internal.smb1.com.SmbComClose;
import jcifs.internal.smb1.com.SmbComFindClose2;
import jcifs.internal.smb1.trans.nt.NtTransQuerySecurityDesc;
import jcifs.util.transport.TransportException;
/**
* This class encapsulates the logic for switching tree connections
*
* Switching trees can occur either when the tree has been disconnected by failure or idle-timeout - as well as on
* DFS referrals.
*
* @author mbechler
*
*/
class SmbTreeConnection {
private static final Logger log = LoggerFactory.getLogger(SmbTreeConnection.class);
private final CIFSContext ctx;
private final SmbTreeConnection delegate;
private SmbTreeImpl tree;
private volatile boolean treeAcquired;
private volatile boolean delegateAcquired;
private SmbTransportInternal exclusiveTransport;
private boolean nonPooled;
private final AtomicLong usageCount = new AtomicLong();
private static final Random RAND = new Random();
protected SmbTreeConnection ( CIFSContext ctx ) {
this.ctx = ctx;
this.delegate = null;
}
protected SmbTreeConnection ( SmbTreeConnection treeConnection ) {
this.ctx = treeConnection.ctx;
this.delegate = treeConnection;
}
static SmbTreeConnection create ( CIFSContext c ) {
if ( c.getConfig().isTraceResourceUsage() ) {
return new SmbTreeConnectionTrace(c);
}
return new SmbTreeConnection(c);
}
static SmbTreeConnection create ( SmbTreeConnection c ) {
if ( c.ctx.getConfig().isTraceResourceUsage() ) {
return new SmbTreeConnectionTrace(c);
}
return new SmbTreeConnection(c);
}
/**
* @return the active configuration
*/
public Configuration getConfig () {
return this.ctx.getConfig();
}
private synchronized SmbTreeImpl getTree () {
SmbTreeImpl t = this.tree;
if ( t != null ) {
return t.acquire(false);
}
else if ( this.delegate != null ) {
this.tree = this.delegate.getTree();
return this.tree;
}
return t;
}
/**
* @return
*/
private synchronized SmbTreeImpl getTreeInternal () {
SmbTreeImpl t = this.tree;
if ( t != null ) {
return t;
}
if ( this.delegate != null ) {
return this.delegate.getTreeInternal();
}
return null;
}
/**
* @param t
*/
private synchronized void switchTree ( SmbTreeImpl t ) {
try ( SmbTreeImpl old = getTree() ) {
if ( old == t ) {
return;
}
boolean wasAcquired = this.treeAcquired;
log.debug("Switching tree");
if ( t != null ) {
log.debug("Acquired tree on switch " + t);
t.acquire();
this.treeAcquired = true;
}
else {
this.treeAcquired = false;
}
this.tree = t;
if ( old != null ) {
if ( wasAcquired ) {
// release
old.release(true);
}
}
if ( this.delegate != null && this.delegateAcquired ) {
log.debug("Releasing delegate");
this.delegateAcquired = false;
this.delegate.release();
}
}
}
/**
* @return tree connection with increased usage count
*/
public SmbTreeConnection acquire () {
long usage = this.usageCount.incrementAndGet();
if ( log.isTraceEnabled() ) {
log.trace("Acquire tree connection " + usage + " " + this);
}
if ( usage == 1 ) {
synchronized ( this ) {
try ( SmbTreeImpl t = getTree() ) {
if ( t != null ) {
if ( !this.treeAcquired ) {
if ( log.isDebugEnabled() ) {
log.debug("Acquire tree on first usage " + t);
}
t.acquire();
this.treeAcquired = true;
}
}
}
if ( this.delegate != null && !this.delegateAcquired ) {
log.debug("Acquire delegate on first usage");
this.delegate.acquire();
this.delegateAcquired = true;
}
}
}
return this;
}
/**
*
*/
public void release () {
long usage = this.usageCount.decrementAndGet();
if ( log.isTraceEnabled() ) {
log.trace("Release tree connection " + usage + " " + this);
}
if ( usage == 0 ) {
synchronized ( this ) {
try ( SmbTreeImpl t = getTree() ) {
if ( this.treeAcquired && t != null ) {
if ( log.isDebugEnabled() ) {
log.debug("Tree connection no longer in use, release tree " + t);
}
this.treeAcquired = false;
t.release();
}
}
if ( this.delegate != null && this.delegateAcquired ) {
this.delegateAcquired = false;
this.delegate.release();
}
}
SmbTransportInternal et = this.exclusiveTransport;
if ( et != null ) {
synchronized ( this ) {
try {
log.debug("Disconnecting exclusive transport");
this.exclusiveTransport = null;
this.tree = null;
this.treeAcquired = false;
et.close();
et.disconnect(false, false);
}
catch ( Exception e ) {
log.error("Failed to close exclusive transport", e);
}
}
}
}
else if ( usage < 0 ) {
log.error("Usage count dropped below zero " + this);
throw new RuntimeCIFSException("Usage count dropped below zero");
}
}
protected void checkRelease () {
if ( isConnected() && this.usageCount.get() != 0 ) {
log.warn("Tree connection was not properly released " + this);
}
}
synchronized void disconnect ( boolean inError ) {
try ( SmbSessionImpl session = getSession() ) {
if ( session == null ) {
return;
}
try ( SmbTransportImpl transport = session.getTransport() ) {
synchronized ( transport ) {
SmbTreeImpl t = getTreeInternal();
if ( t != null ) {
try {
t.treeDisconnect(inError, true);
}
finally {
this.tree = null;
this.treeAcquired = false;
}
}
else {
this.delegate.disconnect(inError);
}
}
}
}
}
T send ( SmbResourceLocatorImpl loc, CommonServerMessageBlockRequest request, T response,
RequestParam... params ) throws CIFSException {
return send(loc, request, response, params.length == 0 ? EnumSet.noneOf(RequestParam.class) : EnumSet.copyOf(Arrays.asList(params)));
}
T send ( SmbResourceLocatorImpl loc, CommonServerMessageBlockRequest request, T response,
Set params ) throws CIFSException {
CIFSException last = null;
RequestWithPath rpath = ( request instanceof RequestWithPath ) ? (RequestWithPath) request : null;
String savedPath = rpath != null ? rpath.getPath() : null;
String savedFullPath = rpath != null ? rpath.getFullUNCPath() : null;
String fullPath = "\\" + loc.getServer() + "\\" + loc.getShare() + loc.getUNCPath();
int maxRetries = this.ctx.getConfig().getMaxRequestRetries();
for ( int retries = 1; retries <= maxRetries; retries++ ) {
if ( rpath != null ) {
rpath.setFullUNCPath(null, null, fullPath);
}
try {
return send0(loc, request, response, params);
}
catch ( SmbException smbe ) {
// Retrying only makes sense if the invalid parameter is an tree id. If we have a stale file descriptor
// retrying make no sense, as it will never become available again.
if ( params.contains(RequestParam.NO_RETRY)
|| ( ! ( smbe.getCause() instanceof TransportException ) ) && smbe.getNtStatus() != NtStatus.NT_STATUS_INVALID_PARAMETER ) {
log.debug("Not retrying", smbe);
throw smbe;
}
log.debug("send", smbe);
last = smbe;
}
catch ( CIFSException e ) {
log.debug("send", e);
last = e;
}
// If we get here, we got the 'The Parameter is incorrect' error or a transport exception
// Disconnect and try again from scratch.
if ( log.isDebugEnabled() ) {
log.debug(String.format("Retrying (%d/%d) request %s", retries, maxRetries, request));
}
// should we disconnect the transport here? otherwise we make an additional attempt to detect that if the
// server closed the connection as a result
log.debug("Disconnecting tree on send retry", last);
disconnect(true);
if ( retries >= maxRetries ) {
break;
}
try {
if ( retries != 1 ) {
// backoff, but don't delay the first attempt as there are various reasons that can be fixed
// immediately
Thread.sleep(500 + RAND.nextInt(1000));
}
}
catch ( InterruptedException e ) {
log.debug("interrupted sleep in send", e);
}
if ( request != null ) {
log.debug("Restting request");
request.reset();
}
if ( rpath != null ) {
// resolveDfs() and tree.send() modify the request packet.
// I want to restore it before retrying. request.reset()
// restores almost everything that was modified, except the path.
rpath.setPath(savedPath);
rpath.setFullUNCPath(rpath.getDomain(), rpath.getServer(), savedFullPath);
}
if ( response != null ) {
response.reset();
}
try ( SmbTreeHandle th = connectWrapException(loc) ) {
log.debug("Have new tree connection for retry");
}
catch ( SmbException e ) {
log.debug("Failed to connect tree on retry", e);
last = e;
}
}
if ( last != null ) {
log.debug("All attempts have failed, last exception", last);
throw last;
}
throw new SmbException("All attempts failed, but no exception");
}
private T send0 ( SmbResourceLocatorImpl loc, CommonServerMessageBlockRequest request, T response,
Set params ) throws CIFSException, DfsReferral {
for ( int limit = 10; limit > 0; limit-- ) {
if ( request instanceof RequestWithPath ) {
ensureDFSResolved(loc, (RequestWithPath) request);
}
try ( SmbTreeImpl t = getTree() ) {
if ( t == null ) {
throw new CIFSException("Failed to get tree connection");
} ;
return t.send(request, response, params);
}
catch ( DfsReferral dre ) {
if ( dre.getData().unwrap(DfsReferralDataInternal.class).isResolveHashes() ) {
throw dre;
}
request.reset();
log.trace("send0", dre);
}
}
throw new CIFSException("Loop in DFS referrals");
}
/**
* @param loc
* @return tree handle
* @throws SmbException
*/
public SmbTreeHandleImpl connectWrapException ( SmbResourceLocatorImpl loc ) throws SmbException {
try {
return connect(loc);
}
catch ( UnknownHostException uhe ) {
throw new SmbException("Failed to connect to server", uhe);
}
catch ( SmbException se ) {
throw se;
}
catch ( IOException ioe ) {
throw new SmbException("Failed to connect to server", ioe);
}
}
/**
* @param loc
* @return tree handle
* @throws IOException
*/
public synchronized SmbTreeHandleImpl connect ( SmbResourceLocatorImpl loc ) throws IOException {
try ( SmbSessionImpl session = getSession() ) {
if ( isConnected() ) {
try ( SmbTransportImpl transport = session.getTransport() ) {
if ( transport.isDisconnected() || transport.getRemoteHostName() == null ) {
/*
* Tree/session thinks it is connected but transport disconnected
* under it, reset tree to reflect the truth.
*/
log.debug("Disconnecting failed tree and session");
disconnect(true);
}
}
}
if ( isConnected() ) {
log.trace("Already connected");
return new SmbTreeHandleImpl(loc, this);
}
return connectHost(loc, loc.getServerWithDfs());
}
}
/**
* @return whether we have a valid tree connection
*/
@SuppressWarnings ( "resource" )
public synchronized boolean isConnected () {
SmbTreeImpl t = getTreeInternal();
return t != null && t.isConnected();
}
/**
*
* @param loc
* @param host
* @return tree handle
* @throws IOException
*/
public synchronized SmbTreeHandleImpl connectHost ( SmbResourceLocatorImpl loc, String host ) throws IOException {
return connectHost(loc, host, null);
}
/**
*
* @param loc
* @param host
* @param referral
* @return tree handle
* @throws IOException
*/
public synchronized SmbTreeHandleImpl connectHost ( SmbResourceLocatorImpl loc, String host, DfsReferralData referral ) throws IOException {
String targetDomain = null;
try ( SmbTreeImpl t = getTree() ) {
if ( t != null ) {
if ( log.isDebugEnabled() ) {
log.debug("Tree is " + t);
}
if ( Objects.equals(loc.getShare(), t.getShare()) ) {
try ( SmbSessionImpl session = t.getSession() ) {
targetDomain = session.getTargetDomain();
if ( !session.isFailed() ) {
try ( SmbTransportImpl trans = session.getTransport();
SmbTreeImpl ct = connectTree(loc, host, t.getShare(), trans, t, null) ) {
switchTree(ct);
return new SmbTreeHandleImpl(loc, this);
}
}
log.debug("Session no longer valid");
}
}
}
}
String hostName = loc.getServerWithDfs();
String path = ( loc.getType() == SmbConstants.TYPE_SHARE || loc.getUNCPath() == null || "\\".equals(loc.getUNCPath()) ) ? null
: loc.getUNCPath();
String share = loc.getShare();
DfsReferralData start = referral != null ? referral : this.ctx.getDfs().resolve(this.ctx, hostName, loc.getShare(), path);
DfsReferralData dr = start;
IOException last = null;
do {
if ( dr != null ) {
targetDomain = dr.getDomain();
host = dr.getServer().toLowerCase(Locale.ROOT);
share = dr.getShare();
}
try {
if ( this.nonPooled ) {
if ( log.isDebugEnabled() ) {
log.debug("Using exclusive transport for " + this);
}
this.exclusiveTransport = this.ctx.getTransportPool()
.getSmbTransport(this.ctx, host, loc.getPort(), true, loc.shouldForceSigning()).unwrap(SmbTransportInternal.class);
SmbTransportInternal trans = this.exclusiveTransport;
try ( SmbSessionInternal smbSession = trans.getSmbSession(this.ctx, host, targetDomain).unwrap(SmbSessionInternal.class);
SmbTreeImpl uct = smbSession.getSmbTree(share, null).unwrap(SmbTreeImpl.class);
SmbTreeImpl ct = connectTree(loc, host, share, trans, uct, dr) ) {
if ( dr != null ) {
ct.setTreeReferral(dr);
if ( dr != start ) {
dr.unwrap(DfsReferralDataInternal.class).replaceCache();
}
}
switchTree(ct);
return new SmbTreeHandleImpl(loc, this);
}
}
try ( SmbTransportInternal trans = this.ctx.getTransportPool()
.getSmbTransport(this.ctx, host, loc.getPort(), false, loc.shouldForceSigning()).unwrap(SmbTransportInternal.class);
SmbSessionInternal smbSession = trans.getSmbSession(this.ctx, host, targetDomain).unwrap(SmbSessionInternal.class);
SmbTreeImpl uct = smbSession.getSmbTree(share, null).unwrap(SmbTreeImpl.class);
SmbTreeImpl ct = connectTree(loc, host, share, trans, uct, dr) ) {
if ( dr != null ) {
ct.setTreeReferral(dr);
if ( dr != start ) {
dr.unwrap(DfsReferralDataInternal.class).replaceCache();
}
}
switchTree(ct);
return new SmbTreeHandleImpl(loc, this);
}
}
catch ( IOException e ) {
last = e;
log.debug("Referral failed, trying next", e);
}
if ( dr != null ) {
dr = dr.next();
}
}
while ( dr != start );
throw last;
}
/**
* @param loc
* @param addr
* @param trans
* @param t
* @throws CIFSException
*/
private SmbTreeImpl connectTree ( SmbResourceLocatorImpl loc, String addr, String share, SmbTransportInternal trans, SmbTreeImpl t,
DfsReferralData referral ) throws CIFSException {
if ( log.isDebugEnabled() && trans.isSigningOptional() && !loc.isIPC() && !this.ctx.getConfig().isSigningEnforced() ) {
log.debug("Signatures for file enabled but not required " + this);
}
if ( referral != null ) {
t.markDomainDfs();
}
try {
if ( log.isTraceEnabled() ) {
log.trace("doConnect: " + addr);
}
t.treeConnect(null, null);
return t.acquire();
}
catch ( SmbAuthException sae ) {
log.debug("Authentication failed", sae);
return retryAuthentication(loc, share, trans, t, referral, sae);
}
}
private SmbTreeImpl retryAuthentication ( SmbResourceLocatorImpl loc, String share, SmbTransportInternal trans, SmbTreeImpl t,
DfsReferralData referral, SmbAuthException sae ) throws SmbAuthException, CIFSException {
try ( SmbSessionImpl treesess = t.getSession() ) {
if ( treesess.getCredentials().isAnonymous() || treesess.getCredentials().isGuest() ) {
// refresh anonymous session or fallback to anonymous from guest login
try ( SmbSessionInternal s = trans
.getSmbSession(this.ctx.withAnonymousCredentials(), treesess.getTargetHost(), treesess.getTargetDomain())
.unwrap(SmbSessionInternal.class);
SmbTreeImpl tr = s.getSmbTree(share, null).unwrap(SmbTreeImpl.class) ) {
tr.treeConnect(null, null);
log.debug("Anonymous retry succeeded");
return tr.acquire();
}
catch ( Exception e ) {
log.debug("Retry also failed", e);
throw sae;
}
}
else if ( this.ctx.renewCredentials(loc.getURL().toString(), sae) ) {
log.debug("Trying to renew credentials after auth error");
try ( SmbSessionInternal s = trans.getSmbSession(this.ctx, treesess.getTargetHost(), treesess.getTargetDomain())
.unwrap(SmbSessionInternal.class);
SmbTreeImpl tr = s.getSmbTree(share, null).unwrap(SmbTreeImpl.class) ) {
if ( referral != null ) {
tr.markDomainDfs();
}
tr.treeConnect(null, null);
return tr.acquire();
}
}
else {
throw sae;
}
}
}
SmbResourceLocator ensureDFSResolved ( SmbResourceLocatorImpl loc ) throws CIFSException {
return ensureDFSResolved(loc, null);
}
SmbResourceLocator ensureDFSResolved ( SmbResourceLocatorImpl loc, RequestWithPath request ) throws CIFSException {
if ( request instanceof SmbComClose )
return loc;
for ( int retries = 0; retries < 1 + this.ctx.getConfig().getMaxRequestRetries(); retries++ ) {
try {
return resolveDfs0(loc, request);
}
catch ( SmbException smbe ) {
// The connection may have been dropped?
if ( smbe.getNtStatus() != NtStatus.NT_STATUS_NOT_FOUND && ! ( smbe.getCause() instanceof TransportException ) ) {
throw smbe;
}
log.debug("resolveDfs", smbe);
}
// If we get here, we apparently have a bad connection.
// Disconnect and try again.
if ( log.isDebugEnabled() ) {
log.debug("Retrying (" + retries + ") resolveDfs: " + request);
}
log.debug("Disconnecting tree on DFS retry");
disconnect(true);
try {
Thread.sleep(500 + RAND.nextInt(5000));
}
catch ( InterruptedException e ) {
log.debug("resolveDfs", e);
}
try ( SmbTreeHandle th = connectWrapException(loc) ) {}
}
return loc;
}
private SmbResourceLocator resolveDfs0 ( SmbResourceLocatorImpl loc, RequestWithPath request ) throws CIFSException {
try ( SmbTreeHandleImpl th = connectWrapException(loc);
SmbSessionImpl session = th.getSession();
SmbTransportImpl transport = session.getTransport();
SmbTreeImpl t = getTree() ) {
transport.ensureConnected();
String rpath = request != null ? request.getPath() : loc.getUNCPath();
String rfullpath = request != null ? request.getFullUNCPath() : ( '\\' + loc.getServer() + '\\' + loc.getShare() + loc.getUNCPath() );
if ( t.isInDomainDfs() || !t.isPossiblyDfs() ) {
if ( t.isInDomainDfs() ) {
// need to adjust request path
DfsReferralData dr = t.getTreeReferral();
if ( dr != null ) {
if ( log.isDebugEnabled() ) {
log.debug(String.format("Need to adjust request path %s (full: %s) -> %s", rpath, rfullpath, dr));
}
String dunc = loc.handleDFSReferral(dr, rpath);
if ( request != null ) {
request.setPath(dunc);
}
return loc;
}
// fallthrough to normal handling
log.debug("No tree referral but in DFS");
}
else {
log.trace("Not in DFS");
return loc;
}
}
if ( request != null ) {
request.setFullUNCPath(session.getTargetDomain(), session.getTargetHost(), rfullpath);
}
// for standalone DFS we could be checking for a referral here, too
DfsReferralData dr = this.ctx.getDfs().resolve(this.ctx, loc.getServer(), loc.getShare(), loc.getUNCPath());
if ( dr != null ) {
if ( log.isDebugEnabled() ) {
log.debug("Resolved " + rfullpath + " -> " + dr);
}
String dunc = loc.handleDFSReferral(dr, rpath);
if ( request != null ) {
request.setPath(dunc);
}
if ( !t.getShare().equals(dr.getShare()) ) {
// this should only happen for standalone roots or if the DC/domain root lookup failed
IOException last;
DfsReferralData start = dr;
do {
if ( log.isDebugEnabled() ) {
log.debug("Need to switch tree for " + dr);
}
try ( SmbTreeHandleImpl nt = connectHost(loc, session.getTargetHost(), dr) ) {
log.debug("Switched tree");
return loc;
}
catch ( IOException e ) {
log.debug("Failed to connect tree", e);
last = e;
}
dr = dr.next();
}
while ( dr != start );
throw new CIFSException("All referral tree connections failed", last);
}
return loc;
}
else if ( t.isInDomainDfs() && ! ( request instanceof NtTransQuerySecurityDesc ) && ! ( request instanceof SmbComClose )
&& ! ( request instanceof SmbComFindClose2 ) ) {
if ( log.isDebugEnabled() ) {
log.debug("No referral available for " + rfullpath);
}
throw new CIFSException("No referral but in domain DFS " + rfullpath);
}
else {
log.trace("Not in DFS");
return loc;
}
}
}
/**
* Use a exclusive connection for this tree
*
* If an exclusive connection is used the caller must make sure that the tree handle is kept alive,
* otherwise the connection will be disconnected once the usage drops to zero.
*
* @param np
* whether to use an exclusive connection
*/
void setNonPooled ( boolean np ) {
this.nonPooled = np;
}
/**
* @return the currently connected tid
*/
@SuppressWarnings ( "resource" )
public long getTreeId () {
SmbTreeImpl t = getTreeInternal();
if ( t == null ) {
return -1;
}
return t.getTreeNum();
}
/**
*
* Only call this method while holding a tree handle
*
* @return session that this file has been loaded through
*/
@SuppressWarnings ( "resource" )
public SmbSessionImpl getSession () {
SmbTreeImpl t = getTreeInternal();
if ( t != null ) {
return t.getSession();
}
return null;
}
/**
*
* Only call this method while holding a tree handle
*
* @param cap
* @return whether the capability is available
* @throws SmbException
*/
public boolean hasCapability ( int cap ) throws SmbException {
try ( SmbSessionImpl s = getSession() ) {
if ( s != null ) {
try ( SmbTransportImpl transport = s.getTransport() ) {
return transport.hasCapability(cap);
}
}
throw new SmbException("Not connected");
}
}
/**
* Only call this method while holding a tree handle
*
* @return the connected tree type
*/
public int getTreeType () {
try ( SmbTreeImpl t = getTree() ) {
return t.getTreeType();
}
}
/**
*
* Only call this method while holding a tree handle
*
* @return the share we are connected to
*/
public String getConnectedShare () {
try ( SmbTreeImpl t = getTree() ) {
return t.getShare();
}
}
/**
*
* Only call this method while holding a tree handle
*
* @param other
* @return whether the connection refers to the same tree
*/
public boolean isSame ( SmbTreeConnection other ) {
try ( SmbTreeImpl t1 = getTree();
SmbTreeImpl t2 = other.getTree() ) {
return t1 == t2;
}
}
}