| /* |
| * Copyright (C) 2008, 2010 Google Inc. |
| * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com> |
| * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> |
| * Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> and others |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Distribution License v. 1.0 which is available at |
| * https://www.eclipse.org/org/documents/edl-v10.php. |
| * |
| * SPDX-License-Identifier: BSD-3-Clause |
| */ |
| |
| package org.eclipse.jgit.transport; |
| |
| import static org.eclipse.jgit.transport.GitProtocolConstants.COMMAND_LS_REFS; |
| import static org.eclipse.jgit.transport.GitProtocolConstants.OPTION_AGENT; |
| import static org.eclipse.jgit.transport.GitProtocolConstants.REF_ATTR_PEELED; |
| import static org.eclipse.jgit.transport.GitProtocolConstants.REF_ATTR_SYMREF_TARGET; |
| import static org.eclipse.jgit.transport.GitProtocolConstants.VERSION_1; |
| import static org.eclipse.jgit.transport.GitProtocolConstants.VERSION_2; |
| |
| import java.io.EOFException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.text.MessageFormat; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| |
| import org.eclipse.jgit.annotations.NonNull; |
| import org.eclipse.jgit.errors.InvalidObjectIdException; |
| import org.eclipse.jgit.errors.NoRemoteRepositoryException; |
| import org.eclipse.jgit.errors.PackProtocolException; |
| import org.eclipse.jgit.errors.RemoteRepositoryException; |
| import org.eclipse.jgit.errors.TransportException; |
| import org.eclipse.jgit.internal.JGitText; |
| import org.eclipse.jgit.lib.Constants; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.ObjectIdRef; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.lib.SymbolicRef; |
| import org.eclipse.jgit.util.StringUtils; |
| import org.eclipse.jgit.util.io.InterruptTimer; |
| import org.eclipse.jgit.util.io.TimeoutInputStream; |
| import org.eclipse.jgit.util.io.TimeoutOutputStream; |
| |
| /** |
| * Base helper class for pack-based operations implementations. Provides partial |
| * implementation of pack-protocol - refs advertising and capabilities support, |
| * and some other helper methods. |
| * |
| * @see BasePackFetchConnection |
| * @see BasePackPushConnection |
| */ |
| abstract class BasePackConnection extends BaseConnection { |
| |
| /** The capability prefix for a symlink */ |
| protected static final String CAPABILITY_SYMREF_PREFIX = "symref="; //$NON-NLS-1$ |
| |
| /** The repository this transport fetches into, or pushes out of. */ |
| protected final Repository local; |
| |
| /** Remote repository location. */ |
| protected final URIish uri; |
| |
| /** A transport connected to {@link BasePackConnection#uri}. */ |
| protected final Transport transport; |
| |
| /** Low-level input stream, if a timeout was configured. */ |
| protected TimeoutInputStream timeoutIn; |
| |
| /** Low-level output stream, if a timeout was configured. */ |
| protected TimeoutOutputStream timeoutOut; |
| |
| /** |
| * Timer to manage {@link #timeoutIn} and |
| * {@link BasePackConnection#timeoutOut}. |
| */ |
| private InterruptTimer myTimer; |
| |
| /** Input stream reading from the remote. */ |
| protected InputStream in; |
| |
| /** Output stream sending to the remote. */ |
| protected OutputStream out; |
| |
| /** Packet line decoder around {@link BasePackConnection#in}. */ |
| protected PacketLineIn pckIn; |
| |
| /** Packet line encoder around {@link BasePackConnection#out}. */ |
| protected PacketLineOut pckOut; |
| |
| /** |
| * Send {@link PacketLineOut#end()} before closing |
| * {@link BasePackConnection#out}? |
| */ |
| protected boolean outNeedsEnd; |
| |
| /** True if this is a stateless RPC connection. */ |
| protected boolean statelessRPC; |
| |
| /** Capability tokens advertised by the remote side. */ |
| private final Map<String, String> remoteCapabilities = new HashMap<>(); |
| |
| /** Extra objects the remote has, but which aren't offered as refs. */ |
| protected final Set<ObjectId> additionalHaves = new HashSet<>(); |
| |
| private TransferConfig.ProtocolVersion protocol = TransferConfig.ProtocolVersion.V0; |
| |
| BasePackConnection(PackTransport packTransport) { |
| transport = (Transport) packTransport; |
| local = transport.local; |
| uri = transport.uri; |
| } |
| |
| TransferConfig.ProtocolVersion getProtocolVersion() { |
| return protocol; |
| } |
| |
| void setProtocolVersion(@NonNull TransferConfig.ProtocolVersion protocol) { |
| this.protocol = protocol; |
| } |
| |
| /** |
| * Configure this connection with the directional pipes. |
| * |
| * @param myIn |
| * input stream to receive data from the peer. Caller must ensure |
| * the input is buffered, otherwise read performance may suffer. |
| * @param myOut |
| * output stream to transmit data to the peer. Caller must ensure |
| * the output is buffered, otherwise write performance may |
| * suffer. |
| */ |
| protected final void init(InputStream myIn, OutputStream myOut) { |
| final int timeout = transport.getTimeout(); |
| if (timeout > 0) { |
| final Thread caller = Thread.currentThread(); |
| if (myTimer == null) { |
| myTimer = new InterruptTimer(caller.getName() + "-Timer"); //$NON-NLS-1$ |
| } |
| timeoutIn = new TimeoutInputStream(myIn, myTimer); |
| timeoutOut = new TimeoutOutputStream(myOut, myTimer); |
| timeoutIn.setTimeout(timeout * 1000); |
| timeoutOut.setTimeout(timeout * 1000); |
| myIn = timeoutIn; |
| myOut = timeoutOut; |
| } |
| |
| in = myIn; |
| out = myOut; |
| |
| pckIn = new PacketLineIn(in); |
| pckOut = new PacketLineOut(out); |
| outNeedsEnd = true; |
| } |
| |
| /** |
| * Reads the advertised references through the initialized stream. |
| * <p> |
| * Subclass implementations may call this method only after setting up the |
| * input and output streams with {@link #init(InputStream, OutputStream)}. |
| * <p> |
| * If any errors occur, this connection is automatically closed by invoking |
| * {@link #close()} and the exception is wrapped (if necessary) and thrown |
| * as a {@link org.eclipse.jgit.errors.TransportException}. |
| * |
| * @return {@code true} if the refs were read; {@code false} otherwise |
| * indicating that {@link #lsRefs} must be called |
| * |
| * @throws org.eclipse.jgit.errors.TransportException |
| * the reference list could not be scanned. |
| */ |
| protected boolean readAdvertisedRefs() throws TransportException { |
| try { |
| return readAdvertisedRefsImpl(); |
| } catch (TransportException err) { |
| close(); |
| throw err; |
| } catch (IOException | RuntimeException err) { |
| close(); |
| throw new TransportException(err.getMessage(), err); |
| } |
| } |
| |
| private String readLine() throws IOException { |
| String line = pckIn.readString(); |
| if (PacketLineIn.isEnd(line)) { |
| return null; |
| } |
| if (line.startsWith("ERR ")) { //$NON-NLS-1$ |
| // This is a customized remote service error. |
| // Users should be informed about it. |
| throw new RemoteRepositoryException(uri, line.substring(4)); |
| } |
| return line; |
| } |
| |
| private boolean readAdvertisedRefsImpl() throws IOException { |
| final Map<String, Ref> avail = new LinkedHashMap<>(); |
| final Map<String, String> symRefs = new LinkedHashMap<>(); |
| for (boolean first = true;; first = false) { |
| String line; |
| |
| if (first) { |
| boolean isV1 = false; |
| try { |
| line = readLine(); |
| } catch (EOFException e) { |
| throw noRepository(e); |
| } |
| if (line != null && VERSION_1.equals(line)) { |
| // Same as V0, except for this extra line. We shouldn't get |
| // it since we never request V1. |
| setProtocolVersion(TransferConfig.ProtocolVersion.V0); |
| isV1 = true; |
| line = readLine(); |
| } |
| if (line == null) { |
| break; |
| } |
| final int nul = line.indexOf('\0'); |
| if (nul >= 0) { |
| // Protocol V0: The first line (if any) may contain |
| // "hidden" capability values after a NUL byte. |
| for (String capability : line.substring(nul + 1) |
| .split(" ")) { //$NON-NLS-1$ |
| if (capability.startsWith(CAPABILITY_SYMREF_PREFIX)) { |
| String[] parts = capability |
| .substring( |
| CAPABILITY_SYMREF_PREFIX.length()) |
| .split(":", 2); //$NON-NLS-1$ |
| if (parts.length == 2) { |
| symRefs.put(parts[0], parts[1]); |
| } |
| } else { |
| addCapability(capability); |
| } |
| } |
| line = line.substring(0, nul); |
| setProtocolVersion(TransferConfig.ProtocolVersion.V0); |
| } else if (!isV1 && VERSION_2.equals(line)) { |
| // Protocol V2: remaining lines are capabilities as |
| // key=value pairs |
| setProtocolVersion(TransferConfig.ProtocolVersion.V2); |
| readCapabilitiesV2(); |
| // Break out here so that stateless RPC transports get a |
| // chance to set up the output stream. |
| return false; |
| } else { |
| setProtocolVersion(TransferConfig.ProtocolVersion.V0); |
| } |
| } else { |
| line = readLine(); |
| if (line == null) { |
| break; |
| } |
| } |
| |
| // Expecting to get a line in the form "sha1 refname" |
| if (line.length() < 41 || line.charAt(40) != ' ') { |
| throw invalidRefAdvertisementLine(line); |
| } |
| String name = line.substring(41, line.length()); |
| if (first && name.equals("capabilities^{}")) { //$NON-NLS-1$ |
| // special line from git-receive-pack (protocol V0) to show |
| // capabilities when there are no refs to advertise |
| continue; |
| } |
| |
| final ObjectId id = toId(line, line.substring(0, 40)); |
| if (name.equals(".have")) { //$NON-NLS-1$ |
| additionalHaves.add(id); |
| } else { |
| processLineV1(name, id, avail); |
| } |
| } |
| updateWithSymRefs(avail, symRefs); |
| available(avail); |
| return true; |
| } |
| |
| /** |
| * Issue a protocol V2 ls-refs command and read its response. |
| * |
| * @param refSpecs |
| * to produce ref prefixes from if the server supports git |
| * protocol V2 |
| * @param additionalPatterns |
| * to use for ref prefixes if the server supports git protocol V2 |
| * @throws TransportException |
| * if the command could not be run or its output not be read |
| */ |
| protected void lsRefs(Collection<RefSpec> refSpecs, |
| String... additionalPatterns) throws TransportException { |
| try { |
| lsRefsImpl(refSpecs, additionalPatterns); |
| } catch (TransportException err) { |
| close(); |
| throw err; |
| } catch (IOException | RuntimeException err) { |
| close(); |
| throw new TransportException(err.getMessage(), err); |
| } |
| } |
| |
| private void lsRefsImpl(Collection<RefSpec> refSpecs, |
| String... additionalPatterns) throws IOException { |
| pckOut.writeString("command=" + COMMAND_LS_REFS); //$NON-NLS-1$ |
| // Add the user-agent |
| String agent = UserAgent.get(); |
| if (agent != null && isCapableOf(OPTION_AGENT)) { |
| pckOut.writeString(OPTION_AGENT + '=' + agent); |
| } |
| pckOut.writeDelim(); |
| pckOut.writeString("peel"); //$NON-NLS-1$ |
| pckOut.writeString("symrefs"); //$NON-NLS-1$ |
| for (String refPrefix : getRefPrefixes(refSpecs, additionalPatterns)) { |
| pckOut.writeString("ref-prefix " + refPrefix); //$NON-NLS-1$ |
| } |
| pckOut.end(); |
| final Map<String, Ref> avail = new LinkedHashMap<>(); |
| final Map<String, String> symRefs = new LinkedHashMap<>(); |
| for (;;) { |
| String line = readLine(); |
| if (line == null) { |
| break; |
| } |
| // Expecting to get a line in the form "sha1 refname" |
| if (line.length() < 41 || line.charAt(40) != ' ') { |
| throw invalidRefAdvertisementLine(line); |
| } |
| String name = line.substring(41, line.length()); |
| final ObjectId id = toId(line, line.substring(0, 40)); |
| if (name.equals(".have")) { //$NON-NLS-1$ |
| additionalHaves.add(id); |
| } else { |
| processLineV2(line, id, name, avail, symRefs); |
| } |
| } |
| updateWithSymRefs(avail, symRefs); |
| available(avail); |
| } |
| |
| private Collection<String> getRefPrefixes(Collection<RefSpec> refSpecs, |
| String... additionalPatterns) { |
| if (refSpecs.isEmpty() && (additionalPatterns == null |
| || additionalPatterns.length == 0)) { |
| return Collections.emptyList(); |
| } |
| Set<String> patterns = new HashSet<>(); |
| if (additionalPatterns != null) { |
| Arrays.stream(additionalPatterns).filter(Objects::nonNull) |
| .forEach(patterns::add); |
| } |
| for (RefSpec spec : refSpecs) { |
| // TODO: for now we only do protocol V2 for fetch. For push |
| // RefSpecs, the logic would need to be different. (At the |
| // minimum, take spec.getDestination().) |
| String src = spec.getSource(); |
| if (ObjectId.isId(src)) { |
| continue; |
| } |
| if (spec.isWildcard()) { |
| patterns.add(src.substring(0, src.indexOf('*'))); |
| } else { |
| patterns.add(src); |
| patterns.add(Constants.R_REFS + src); |
| patterns.add(Constants.R_HEADS + src); |
| patterns.add(Constants.R_TAGS + src); |
| } |
| } |
| return patterns; |
| } |
| |
| private void readCapabilitiesV2() throws IOException { |
| // In git protocol V2, capabilities are different. If it's a key-value |
| // pair, the key may be a command name, and the value a space-separated |
| // list of capabilities for that command. We still store it in the same |
| // map as for protocol v0/v1. Protocol v2 code has to account for this. |
| for (;;) { |
| String line = readLine(); |
| if (line == null) { |
| break; |
| } |
| addCapability(line); |
| } |
| } |
| |
| private void addCapability(String capability) { |
| String parts[] = capability.split("=", 2); //$NON-NLS-1$ |
| if (parts.length == 2) { |
| remoteCapabilities.put(parts[0], parts[1]); |
| } |
| remoteCapabilities.put(capability, null); |
| } |
| |
| private ObjectId toId(String line, String value) |
| throws PackProtocolException { |
| try { |
| return ObjectId.fromString(value); |
| } catch (InvalidObjectIdException e) { |
| PackProtocolException ppe = invalidRefAdvertisementLine(line); |
| ppe.initCause(e); |
| throw ppe; |
| } |
| } |
| |
| private void processLineV1(String name, ObjectId id, Map<String, Ref> avail) |
| throws IOException { |
| if (name.endsWith("^{}")) { //$NON-NLS-1$ |
| name = name.substring(0, name.length() - 3); |
| final Ref prior = avail.get(name); |
| if (prior == null) { |
| throw new PackProtocolException(uri, MessageFormat.format( |
| JGitText.get().advertisementCameBefore, name, name)); |
| } |
| if (prior.getPeeledObjectId() != null) { |
| throw duplicateAdvertisement(name + "^{}"); //$NON-NLS-1$ |
| } |
| avail.put(name, new ObjectIdRef.PeeledTag(Ref.Storage.NETWORK, name, |
| prior.getObjectId(), id)); |
| } else { |
| final Ref prior = avail.put(name, new ObjectIdRef.PeeledNonTag( |
| Ref.Storage.NETWORK, name, id)); |
| if (prior != null) { |
| throw duplicateAdvertisement(name); |
| } |
| } |
| } |
| |
| private void processLineV2(String line, ObjectId id, String rest, |
| Map<String, Ref> avail, Map<String, String> symRefs) |
| throws IOException { |
| String[] parts = rest.split(" "); //$NON-NLS-1$ |
| String name = parts[0]; |
| // Two attributes possible, symref-target or peeled |
| String symRefTarget = null; |
| String peeled = null; |
| for (int i = 1; i < parts.length; i++) { |
| if (parts[i].startsWith(REF_ATTR_SYMREF_TARGET)) { |
| if (symRefTarget != null) { |
| throw new PackProtocolException(uri, MessageFormat.format( |
| JGitText.get().duplicateRefAttribute, line)); |
| } |
| symRefTarget = parts[i] |
| .substring(REF_ATTR_SYMREF_TARGET.length()); |
| } else if (parts[i].startsWith(REF_ATTR_PEELED)) { |
| if (peeled != null) { |
| throw new PackProtocolException(uri, MessageFormat.format( |
| JGitText.get().duplicateRefAttribute, line)); |
| } |
| peeled = parts[i].substring(REF_ATTR_PEELED.length()); |
| } |
| if (peeled != null && symRefTarget != null) { |
| break; |
| } |
| } |
| Ref idRef; |
| if (peeled != null) { |
| idRef = new ObjectIdRef.PeeledTag(Ref.Storage.NETWORK, name, id, |
| toId(line, peeled)); |
| } else { |
| idRef = new ObjectIdRef.PeeledNonTag(Ref.Storage.NETWORK, name, id); |
| } |
| Ref prior = avail.put(name, idRef); |
| if (prior != null) { |
| throw duplicateAdvertisement(name); |
| } |
| if (!StringUtils.isEmptyOrNull(symRefTarget)) { |
| symRefs.put(name, symRefTarget); |
| } |
| } |
| |
| /** |
| * Updates the given refMap with {@link SymbolicRef}s defined by the given |
| * symRefs. |
| * <p> |
| * For each entry, symRef, in symRefs, whose value is a key in refMap, adds |
| * a new entry to refMap with that same key and value of a new |
| * {@link SymbolicRef} with source=symRef.key and |
| * target=refMap.get(symRef.value), then removes that entry from symRefs. |
| * <p> |
| * If refMap already contains an entry for symRef.key, it is replaced. |
| * </p> |
| * <p> |
| * For example, given: |
| * </p> |
| * |
| * <pre> |
| * refMap.put("refs/heads/main", ref); |
| * symRefs.put("HEAD", "refs/heads/main"); |
| * </pre> |
| * |
| * then: |
| * |
| * <pre> |
| * updateWithSymRefs(refMap, symRefs); |
| * </pre> |
| * |
| * has the <em>effect</em> of: |
| * |
| * <pre> |
| * refMap.put("HEAD", |
| * new SymbolicRef("HEAD", refMap.get(symRefs.remove("HEAD")))) |
| * </pre> |
| * <p> |
| * Any entry in symRefs whose value is not a key in refMap is ignored. Any |
| * circular symRefs are ignored. |
| * </p> |
| * <p> |
| * Upon completion, symRefs will contain only any unresolvable entries. |
| * </p> |
| * |
| * @param refMap |
| * a non-null, modifiable, Map to update, and the provider of |
| * symref targets. |
| * @param symRefs |
| * a non-null, modifiable, Map of symrefs. |
| * @throws NullPointerException |
| * if refMap or symRefs is null |
| */ |
| static void updateWithSymRefs(Map<String, Ref> refMap, Map<String, String> symRefs) { |
| boolean haveNewRefMapEntries = !refMap.isEmpty(); |
| while (!symRefs.isEmpty() && haveNewRefMapEntries) { |
| haveNewRefMapEntries = false; |
| final Iterator<Map.Entry<String, String>> iterator = symRefs.entrySet().iterator(); |
| while (iterator.hasNext()) { |
| final Map.Entry<String, String> symRef = iterator.next(); |
| if (!symRefs.containsKey(symRef.getValue())) { // defer forward reference |
| final Ref r = refMap.get(symRef.getValue()); |
| if (r != null) { |
| refMap.put(symRef.getKey(), new SymbolicRef(symRef.getKey(), r)); |
| haveNewRefMapEntries = true; |
| iterator.remove(); |
| } |
| } |
| } |
| } |
| // If HEAD is still in the symRefs map here, the real ref was not |
| // reported, but we know it must point to the object reported for HEAD. |
| // So fill it in in the refMap. |
| String headRefName = symRefs.get(Constants.HEAD); |
| if (headRefName != null && !refMap.containsKey(headRefName)) { |
| Ref headRef = refMap.get(Constants.HEAD); |
| if (headRef != null) { |
| ObjectId headObj = headRef.getObjectId(); |
| headRef = new ObjectIdRef.PeeledNonTag(Ref.Storage.NETWORK, |
| headRefName, headObj); |
| refMap.put(headRefName, headRef); |
| headRef = new SymbolicRef(Constants.HEAD, headRef); |
| refMap.put(Constants.HEAD, headRef); |
| symRefs.remove(Constants.HEAD); |
| } |
| } |
| } |
| |
| /** |
| * Create an exception to indicate problems finding a remote repository. The |
| * caller is expected to throw the returned exception. |
| * |
| * Subclasses may override this method to provide better diagnostics. |
| * |
| * @param cause |
| * root cause exception |
| * @return a TransportException saying a repository cannot be found and |
| * possibly why. |
| */ |
| protected TransportException noRepository(Throwable cause) { |
| return new NoRemoteRepositoryException(uri, JGitText.get().notFound, |
| cause); |
| } |
| |
| /** |
| * Whether this option is supported |
| * |
| * @param option |
| * option string |
| * @return whether this option is supported |
| */ |
| protected boolean isCapableOf(String option) { |
| return remoteCapabilities.containsKey(option); |
| } |
| |
| /** |
| * Request capability |
| * |
| * @param b |
| * buffer |
| * @param option |
| * option we want |
| * @return {@code true} if the requested option is supported |
| */ |
| protected boolean wantCapability(StringBuilder b, String option) { |
| if (!isCapableOf(option)) |
| return false; |
| b.append(' '); |
| b.append(option); |
| return true; |
| } |
| |
| /** |
| * Return a capability value. |
| * |
| * @param option |
| * to get |
| * @return the value stored, if any. |
| */ |
| protected String getCapability(String option) { |
| return remoteCapabilities.get(option); |
| } |
| |
| /** |
| * Add user agent capability |
| * |
| * @param b |
| * a {@link java.lang.StringBuilder} object. |
| */ |
| protected void addUserAgentCapability(StringBuilder b) { |
| String a = UserAgent.get(); |
| if (a != null && remoteCapabilities.get(OPTION_AGENT) != null) { |
| b.append(' ').append(OPTION_AGENT).append('=').append(a); |
| } |
| } |
| |
| @Override |
| public String getPeerUserAgent() { |
| String agent = remoteCapabilities.get(OPTION_AGENT); |
| return agent != null ? agent : super.getPeerUserAgent(); |
| } |
| |
| private PackProtocolException duplicateAdvertisement(String name) { |
| return new PackProtocolException(uri, MessageFormat.format(JGitText.get().duplicateAdvertisementsOf, name)); |
| } |
| |
| private PackProtocolException invalidRefAdvertisementLine(String line) { |
| return new PackProtocolException(uri, MessageFormat.format(JGitText.get().invalidRefAdvertisementLine, line)); |
| } |
| |
| @Override |
| public void close() { |
| if (out != null) { |
| try { |
| if (outNeedsEnd) { |
| outNeedsEnd = false; |
| pckOut.end(); |
| } |
| out.close(); |
| } catch (IOException err) { |
| // Ignore any close errors. |
| } finally { |
| out = null; |
| pckOut = null; |
| } |
| } |
| |
| if (in != null) { |
| try { |
| in.close(); |
| } catch (IOException err) { |
| // Ignore any close errors. |
| } finally { |
| in = null; |
| pckIn = null; |
| } |
| } |
| |
| if (myTimer != null) { |
| try { |
| myTimer.terminate(); |
| } finally { |
| myTimer = null; |
| timeoutIn = null; |
| timeoutOut = null; |
| } |
| } |
| } |
| |
| /** |
| * Tell the peer we are disconnecting, if it cares to know. |
| */ |
| protected void endOut() { |
| if (outNeedsEnd && out != null) { |
| try { |
| outNeedsEnd = false; |
| pckOut.end(); |
| } catch (IOException e) { |
| try { |
| out.close(); |
| } catch (IOException err) { |
| // Ignore any close errors. |
| } finally { |
| out = null; |
| pckOut = null; |
| } |
| } |
| } |
| } |
| } |