Compartilhamento da área de transferência do Windows com o Web Services

Este aplicativo transfere o conteúdo da área de transferência de um computador para outro usando o ASP .NET Web Services

Publicado em: 29 de março de 2007

Por Brian Trautman

Nível de dificuldade: Fácil

Tempo necessário: 1 a 3 horas

Custo: Gratuito

Software: Visual Basic ou Visual C# Express

Hardware: Nenhum

Download: Download

Nesta página

Histórico
Conclusão
Biografia

Histórico

Já aconteceu de você estar trabalhando em vários computadores ao mesmo tempo e precisar copiar o conteúdo da área de transferência de um computador em outro? Eu sempre quis que existisse uma forma fácil e rápida de mover trechos de textos, capturas de telas e até mesmo arquivos para outro computador com um simples copiar e colar. Se soar interessante para você, continue lendo este artigo.

Eu queria que funcionasse independentemente de ambos os computadores estarem ligados ao mesmo tempo, e não queria ser interrompido por firewalls, NATs etc. Por isso, optei por uma arquitetura baseada em servidor, em vez de ponto-a-ponto. A arquitetura inclui um aplicativo cliente, para transferir o conteúdo da área de transferência ao servidor por meio de uma chamada ao Web Services, um Web service, para armazenar esse conteúdo em cache, e outro componente cliente para recuperar o conteúdo e disponibilizá-lo na área de transferência do computador local.

Para resolver esse problema, precisamos de acesso via programação às áreas de transferência de ambos os computadores, tanto o de origem quanto o de destino. Felizmente, o .NET oferece um invólucro gerenciado da API nativa da área de transferência do Windows que nos permite esse acesso. Os namespaces relevantes são Clipboard for C# e my.Computer.Clipboard. Já que estamos interessados em mover objetos da área de transferência de um computador para outro, primeiro precisamos determinar que tipos de objetos serão colocados na área de transferência em cada uma das ações (copiar texto, imagens e arquivos). Um pequeno trecho de código com o namespace Clipboard nos permitirá iterar em cada um dos objetos da área de transferência em busca dos vários tipos de ações, para verificar com o que estamos lidando.

C#

IDataObject clipData = Clipboard.GetDataObject();

//retrieve an array of strings for all the formats available on the clipboard.
string[] formats = clipData.GetFormats();

//iterate through the list of formats on the clipboard
foreach (string format in formats)
{
//add each objeoct to an arraylist so we can inspect the object types
object dataObject = clipData.GetData(format);
              
}

	

VB

Dim clipData As IDataObject = Clipboard.GetDataObject

Dim formats() As String = clipData.GetFormats

'iterate through the list of formats on the clipboard
For Each format As String In formats
    'add each objeoct to an arraylist so we can inspect the object types
    Dim dataObject As Object = clipData.GetData(format)
Next
	

A captura de tela abaixo mostra o resultado de se copiar texto do Word para a área de transferência. Cada formato na matriz de seqüências de caracteres é uma representação dos dados da área de transferência. Como ainda não se sabe qual será o aplicativo de destino (onde os dados serão colados), a área de transferência guarda os mesmos dados em vários formatos. O objetivo desse projeto é replicar todos esses formatos na área de transferência do computador de destino.

 

Cc564911.image0_thumb(pt-br,MSDN.10).jpg

 

Após a inspeção na área de transferência em busca de objetos tipo texto, imagem e arquivo, ficou fácil determinar os principais tipos com os quais devemos nos preocupar. São eles: "System.IO.MemoryStream", “System.IO.FileStream”, "System.Drawing.Bitmap", e "System.String". Como todas essas informações serão transferidas ao servidor por meio de Web Services, torna-se simples serializar todos os objetos em bytes para a transmissão. Esse procedimento é necessário por uma série de razões, incluindo o fato de que objetos complexos, como MemoryStreams, simplesmente não podem ser serializados e enviados através da chamada ao Web Service, como é possível com objetos Strings. Além disso, alguns objetos são maiores do que o permitido pela chamada ao Web Service. Com isso, seria necessário dividi-los em pedaços menores para a transmissão e, depois, esses objetos precisariam ser montados novamente no servidor, na ordem correta. Além disso, quando o cliente solicitar os itens da área de transferência, será preciso desmontar cada objeto, enviar por meio de um Web Service, retornar o resultado ao cliente e, então, remontá-lo.

O primeiro item a ser criado é uma função de base que lide com a quebra desses fluxos muito grandes em matrizes de bytes mais gerenciáveis, para possibilitar a transmissão ao Web Service. A função abaixo executa essa tarefa enviando blocos de MemoryStream com tamanhos limitados pela constante “byteCount”. Quando o limite é alcançado, o buffer é enviado pela chamada Web Service para armazenamento e montagem no servidor. Se houver 0 bytes a serem enviados, ou quantidade de bytes inferior à constante “byteCount”, enviaremos os elementos restantes do buffer e usaremos o sinalizador “isFinalTransaction” para mostrar ao Web service que este objeto específico está concluído.

C#

private void UploadStreamBlock(string format, string objectType, MemoryStream memStream)
        {
            //each time we enter this function we have a new transaction beginning.  A transaction represents a comlete
            //object on the clipboard and we'll use this on the server side to know how to put the stream back together
            string transactionGuid = System.Guid.NewGuid().ToString();
            memStream.Position = 0;

            byte[] buffer = new byte[byteCount];
            bool isFinalTransaction = false;

            //while the current stream position plus our byte count is less than the length of the stream continue sending as much 
            //as we can.
            while ((memStream.Position + byteCount) <= memStream.Length)
            {
                //if we happen to be on the last byte of the stream set the final transaction flag to true so the server
                //will know that this is the last bit of this transaction to expect.
                if (memStream.Position + byteCount == memStream.Length)
                {
                    isFinalTransaction = true;
                }
                //read the stream into our buffer for transmission over the web service.
                memStream.Read(buffer, 0, byteCount);
                ws.InsertMessageStream(buffer, format, objectType, transactionGuid, isFinalTransaction, clipBoardGUID);
            }

            long remainingBytes = memStream.Length - memStream.Position;
            //if we still have remaining bytes left figure out how many and transmit the last bit of this ojbect over the
            //web service.
            if ((int)remainingBytes > 0)
            {
                byte[] remainingBuffer = new byte[(int)remainingBytes];

                memStream.Read(remainingBuffer, 0, (int)remainingBytes);
                ws.InsertMessageStream(remainingBuffer, format, objectType, transactionGuid, true, clipBoardGUID);
            }

}	
	

VB

Private Sub UploadStreamBlock(ByVal format As String, ByVal objectType As String, ByVal memStream As MemoryStream)
        'each time we enter this function we have a new transaction beginning.  A transaction represents a comlete
        'object on the clipboard and we'll use this on the server side to know how to put the stream back together
        Dim transactionGuid As String = System.Guid.NewGuid.ToString
        memStream.Position = 0
        Dim buffer() As Byte = New Byte((byteCount) - 1) {}
        Dim isFinalTransaction As Boolean = False
        'while the current stream position plus our byte count is less than the length of the stream continue sending as much 
        'as we can.

        While ((memStream.Position + byteCount) _
                    <= memStream.Length)
            'if we happen to be on the last byte of the stream set the final transaction flag to true so the server
            'will know that this is the last bit of this transaction to expect.
            If ((memStream.Position + byteCount) _
                        = memStream.Length) Then
                isFinalTransaction = True
            End If
            'ream the stream into our buffer for transmission over the web service.
            memStream.Read(buffer, 0, byteCount)
            clipService.InsertMessageStream(buffer, format, objectType, transactionGuid, isFinalTransaction, clipBoardGUID)

        End While
        Dim remainingBytes As Long = (memStream.Length - memStream.Position)
        'if we still have remaining bytes left figure out how many and transmit the last bit of this ojbect over the
        'web service.
        If (CType(remainingBytes, Integer) > 0) Then
            Dim remainingBuffer() As Byte = New Byte((CType(remainingBytes, Integer)) - 1) {}
            memStream.Read(remainingBuffer, 0, CType(remainingBytes, Integer))
            clipService.InsertMessageStream(remainingBuffer, format, objectType, transactionGuid, True, clipBoardGUID)
        End If
    End Sub
	

O lado do servidor do Web Service precisa recriar a área de transferência a partir de várias matrizes de bytes. Por isso, é importante que todos os objetos, tipos e formatos sejam preservados para que a área de transferência funcione de forma adequada no computador de destino. Nós usamos o clipBoardGuid para determinar se estamos lidando com uma área de transferência nova ou adicionando objetos a uma instância existente. Usamos o sinalizador isFinalTransaction para saber se a matriz de bytes deveria ser parte de uma transação existente, ou se era a primeira em uma nova transação. Todos os itens da área de transferência são salvos em disco, para uma recuperação posterior por qualquer cliente que os solicite. O código para esse procedimento está abaixo.

C#

[WebMethod]
    public void InsertMessageStream(byte[] buffer, string format, string objectType, string transactionGuid, bool 
    isFinalTransaction, string clipBoardGUID)
    {
        //always base the current directory on the clipboard that we're sending now.
        string clipBoardGUIDDirectory = System.Web.HttpContext.Current.Request.PhysicalApplicationPath + clipBoardGUID;

        try
        {
            //if the directory does not exist then delete all the other directories (clipboard instances) and create a new directory
            //if the directory already exists then this particular transaction is part of the same clipboard so don't do anything.
            //this works because othe clipboardDirectory is based off of the GUID sent from the client.
            if (!Directory.Exists(clipBoardGUIDDirectory))
            {
                string[] dirs = Directory.GetDirectories(System.Web.HttpContext.Current.Request.PhysicalApplicationPath);
                foreach (string dir in dirs)
                {
                    Directory.Delete(dir, true);
                }
                Directory.CreateDirectory(clipBoardGUIDDirectory);
            }
        }
        catch
        {
        }
        //create the filename based on the current transaction, format, and object type.  We will parse this out later
        //so we know how to add this back to the target clipboard.
        string fileName = clipBoardGUIDDirectory + "\\" + transactionGuid + "_" + format + "_" + objectType;
        FileStream fs = new FileStream(fileName, FileMode.Append, FileAccess.Write);
        fs.Position = fs.Length;
        fs.Write(buffer, 0, buffer.Length);
        fs.Close();
}

	

VB

<WebMethod()>
        Public Sub InsertMessageStream(ByVal buffer() As Byte, ByVal format As String, ByVal objectType As String, ByVal 
	transactionGuid As String, ByVal isFinalTransaction As Boolean, ByVal clipBoardGUID As String)
        'always base the current directory on the clipboard that we're sending now.
        Dim clipBoardDataDirectory As String = (System.Web.HttpContext.Current.Request.PhysicalApplicationPath + "\\Clipboard_Data")
        Dim clipBoardGUIDDirectory As String = (clipBoardDataDirectory + ("\\" + clipBoardGUID))
        Try
            'if the directory does not exist then delete all the other directories (clipboard instances) and create a new directory
            'if the directory already exists then this particular transaction is part of the same clipboard so don't do anything.
            'this works because othe clipboardDirectory is based off of the GUID sent from the client.
            If Not Directory.Exists(clipBoardGUIDDirectory) Then
                Dim dirs() As String = Directory.GetDirectories(clipBoardDataDirectory)
                For Each dir As String In dirs
                    Directory.Delete(dir, True)
                Next
                Directory.CreateDirectory(clipBoardGUIDDirectory)
            End If
        Catch

        End Try
        'create the filename based on the current transaction, format, and object type.  We will parse this out later
        'so we know how to add this back to the target clipboard.
        Dim fileName As String = (clipBoardGUIDDirectory + ("\\" _
                    + (transactionGuid + ("_" _
                    + (format + ("_" + objectType))))))
        Dim fs As FileStream = New FileStream(fileName, FileMode.Append, FileAccess.Write)
        fs.Position = fs.Length
        fs.Write(buffer, 0, buffer.Length)
        fs.Close()
End Sub
	

Cada objeto da área de transferência é armazenado em disco para que o cliente possa recuperá-lo posteriormente. Observe na captura de tela abaixo como o nome do arquivo é utilizado para armazenar a ID exclusiva da transação, que inclui dados do objeto, do tipo de objeto e do formato da área de transferência. Todas essas informações são necessárias para a remontagem correta dos itens na área de transferência de destino.

Cc564911.image0_thumb1(pt-br,MSDN.10).jpg

 

Agora que temos uma representação de cada objeto da área de transferência no servidor, precisamos encontrar uma forma de colocar cada item na área de transferência de destino. O método Web Service a seguir fornece um resultado de retorno tipo “ClipboardStream”. O objeto ClipboardStream contém todas as informações necessárias para a remontagem de cada item na área de transferência de destino. Como um Web Service baseia-se em um relacionamento do tipo solicitação-resposta, ele espera que o cliente continue a chamá-lo até que todos os itens da área de transferência tenham sido recebidos com êxito. Além disso, ainda há uma maior complexidade, pois cada item da área de transferência pode ser dividido em vários itens, caso o tamanho definido pela constante “byteCount” seja excedido. Portanto, o computador de destino deve manter o controle de cada solicitação e avisar ao servidor onde parou a última transação por meio da variável “currentByte”. Veja abaixo o código do Web Service.

C#

[WebMethod]
    public ClipboardStream GetMessageStream(string transactionGUID, 
    string[] previousTransactionGUIDs, string clipBoardGUID, long currentByte)
    {
        string clipBoardDataDirectory = System.Web.HttpContext.Current.Request.PhysicalApplicationPath + "Clipboard_Data";
        string clipBoardGUIDDirectory = clipBoardDataDirectory + "\\" + clipBoardGUID;
        string currentTransaction = "";
        bool isLastTransaction = false;


        //if the clipBoardGUID is not empty then we only need to make sure that the directory still exists.
        if (clipBoardGUID != "")
        {
            //if the directory does not exist throw an exception, it must have already been deleted.
            if (!Directory.Exists(clipBoardGUIDDirectory))
            {
                throw new Exception("Requested clipboard does not exist.  It must have been deleted.");
            }
        }
        //if the clipboardGUID is empty then this is the client's first contact with the server and we need
        //to select the available clipboard GUID to return to the user.
        else
        {
            string[] availableClipBoard = Directory.GetDirectories(clipBoardDataDirectory)[0].Split('\\');
            clipBoardGUID = availableClipBoard[availableClipBoard.Length - 1];
            clipBoardGUIDDirectory += clipBoardGUID;
        }

        //we need to get the next transaction.  Each time we finish a transaction we add it to previousTransactionGUIDs
        //at the client end so we know not to send it again.
        currentTransaction = GetCurrentTransaction(clipBoardGUIDDirectory, previousTransactionGUIDs);

        //if the current transaction is null then we're done and there are no more to send to the client
        if (currentTransaction == null)
        {
            return null;
        }

        //open the filestream and set it to the position requested by the client.
        FileStream fs = new FileStream(currentTransaction, FileMode.Open);
        fs.Position = currentByte;

        //determind if this is the last transaction or not for this object so we can let the client know.
        long numBytesToRead = fs.Length - currentByte;
        if (numBytesToRead > byteCount)
        {
            numBytesToRead = byteCount;
            isLastTransaction = false;
        }
        else
        {
            isLastTransaction = true;
        }

        //read the filestream bytes to the buffer and populate the object to return to the client.
        byte[] buffer = new byte[numBytesToRead];
        fs.Read(buffer, 0, (int)numBytesToRead);
        fs.Close();


        FileInfo fi = new FileInfo(currentTransaction);
        ClipboardStream clipboardStream = new ClipboardStream();
        clipboardStream.Buffer = buffer;
        clipboardStream.ClipBoardID = clipBoardGUID;
        clipboardStream.Format = fi.Name.Split('_')[1];
        clipboardStream.ObjectType = fi.Name.Split('_')[2];
        clipboardStream.IsLastTransaction = isLastTransaction;
        clipboardStream.TransactionID = currentTransaction;

        return clipboardStream;

}
	

VB

<WebMethod()> _
    Public Function GetMessageStream(ByVal transactionGUID As String, ByVal previousTransactionGUIDs() As String, 
    ByVal clipBoardGUID As String, ByVal currentByte As Long) As ClipboardStream
        Dim clipBoardDataDirectory As String = (System.Web.HttpContext.Current.Request.PhysicalApplicationPath + "Clipboard_Data")
        Dim clipBoardGUIDDirectory As String = clipBoardDataDirectory
        Dim currentTransaction As String = ""
        Dim isLastTransaction As Boolean = False
        'if the clipBoardGUID is not empty then we only need to make sure that the directory still exists.
        If (clipBoardGUID <> "") Then
            'if the directory does not exist throw an exception, it must have already been deleted.
            If Not Directory.Exists(clipBoardGUIDDirectory) Then
                Throw New Exception("Requested clipboard does not exist.  It must have been deleted.")
            End If
        End If
        'if the clipboardGUID is empty then this is the client's first contact with the server and we need
        'to select the available clipboard GUID to return to the user.
        Dim availableClipBoard() As String = Directory.GetDirectories(clipBoardDataDirectory)(0).Split(Microsoft.VisualBasic.ChrW(92))
        clipBoardGUID = availableClipBoard((availableClipBoard.Length - 1))
        clipBoardGUIDDirectory = (clipBoardGUIDDirectory + "\" + clipBoardGUID)
        'we need to get the next transaction.  Each time we finish a transaction we add it to previousTransactionGUIDs
        'at the client end so we know not to send it again.
        currentTransaction = GetCurrentTransaction(clipBoardGUIDDirectory, previousTransactionGUIDs)
        'if the current transaction is null then we're done and there are no more to send to the client
        If (currentTransaction Is Nothing) Then
            Return Nothing
        End If
        'open the filestream and set it to the position requested by the client.
        Dim fs As FileStream = New FileStream(currentTransaction, FileMode.Open)
        fs.Position = currentByte
        'determind if this is the last transaction or not for this object so we can let the client know.
        Dim numBytesToRead As Long = (fs.Length - currentByte)
        If (numBytesToRead > byteCount) Then
            numBytesToRead = byteCount
            isLastTransaction = False
        Else
            isLastTransaction = True
        End If
        'read the filestream bytes to the buffer and populate the object to return to the client.
        Dim buffer() As Byte = New Byte((numBytesToRead) - 1) {}
        fs.Read(buffer, 0, CType(numBytesToRead, Integer))
        fs.Close()

        Dim fi As FileInfo = New FileInfo(currentTransaction)
        Dim clipboardStream As ClipboardStream = New ClipboardStream
        clipboardStream.Buffer = buffer
        clipboardStream.ClipBoardID = clipBoardGUID
        clipboardStream.Format = fi.Name.Split(Microsoft.VisualBasic.ChrW(95))(1)
        clipboardStream.ObjectType = fi.Name.Split(Microsoft.VisualBasic.ChrW(95))(2)
        clipboardStream.IsLastTransaction = isLastTransaction
        clipboardStream.TransactionID = currentTransaction
        Return clipboardStream
    End Function
	

A última peça que falta para que o nosso projeto funcione corretamente é o código de cliente. Ele recebe o conteúdo da área de transferência do servidor e remonta cada item na ordem correta, adicionando-os à área de transferência com o formato correto à medida que cada um é concluído. Veja abaixo o código de cliente para esse procedimento.

C#

{
            string[] transactionGuids = null;

            ClipboardService.ClipboardStream clipBoardStream = new WindowsApplication1.ClipboardService.ClipboardStream();
            DataObject dataObject = new DataObject();
            clipBoardStream.ClipBoardID = "";
            clipBoardStream.IsLastTransaction = false;
            clipBoardStream.TransactionID = "";
            long currentByte = 0;

            Clipboard.Clear();

            //while we don't get null back keep on contacting the web service to get the next ojbect.
            while (clipBoardStream != null)
            {
                MemoryStream memStream = new MemoryStream();
                //while this is not the last transaction keep on contacting the web service to get the rest
                //of this particular object.
                while (clipBoardStream.IsLastTransaction == false)
                {
                    //contact the web service to get the next transaction
                    clipBoardStream = clipService.GetMessageStream(clipBoardStream.TransactionID, 
		    transactionGuids, clipBoardStream.ClipBoardID, currentByte);

                    if (clipBoardStream != null)
                    {
                        //write the results to the memory stream
                        memStream.Write(clipBoardStream.Buffer, 0, clipBoardStream.Buffer.Length);
                        //increment the current byte so next time we contact the webservice we'll pick up where we left off
                        currentByte = memStream.Position;

                        //if it is the last transaction then we need to place this item onto the clipboard.
                        if (clipBoardStream.IsLastTransaction)
                        {
                            //handle the clipBoardStream appropriately and add it to the dataObject
                            //for posting to the clipblard.
                            HandleFinalTransaction(clipBoardStream, memStream, ref dataObject);

                            //resize the transactionGuids array as necessary and add the current transaction
                            //so next time we contact the web service we won't get this one again.
                            if (transactionGuids == null)
                            {
                                Array.Resize(ref transactionGuids, 1);
                            }
                            else
                            {
                                Array.Resize(ref transactionGuids, transactionGuids.Length + 1);
                            }
                            transactionGuids[transactionGuids.Length - 1] = clipBoardStream.TransactionID;
                        }
                    }
                    else
                    {
                        break;
                    }

                }

                if (clipBoardStream != null)
                {
                    clipBoardStream.IsLastTransaction = false;
                    currentByte = 0;
                }
            }

            Clipboard.SetDataObject(dataObject, true);
	

VB

Dim transactionGuids() As String = Nothing
        Dim clipBoardStream As ClipboardService.ClipboardStream = New ClipboardVB.ClipboardService.ClipboardStream
        Dim dataObject As DataObject = New DataObject
        clipBoardStream.ClipBoardID = ""
        clipBoardStream.IsLastTransaction = False
        clipBoardStream.TransactionID = ""
        Dim currentByte As Long = 0
        Clipboard.Clear()
        'while we don't get null back keep on contacting the web service to get the next ojbect.

        While (Not (clipBoardStream) Is Nothing)
            Dim memStream As MemoryStream = New MemoryStream
            'while this is not the last transaction keep on contacting the web service to get the rest
            'of this particular object.

            While (clipBoardStream.IsLastTransaction = False)
                'contact the web service to get the next transaction

                clipBoardStream = clipService.GetMessageStream(clipBoardStream.TransactionID, transactionGuids, 
		clipBoardStream.ClipBoardID, currentByte)
                If (Not (clipBoardStream) Is Nothing) Then
                    'write the results to the memory stream
                    memStream.Write(clipBoardStream.Buffer, 0, clipBoardStream.Buffer.Length)
                    'increment the current byte so next time we contact the webservice we'll pick up where we left off
                    currentByte = memStream.Position
                    'if it is the last transaction then we need to place this item onto the clipboard.
                    If clipBoardStream.IsLastTransaction Then
                        'handle the clipBoardStream appropriately and add it to the dataObject
                        'for posting to the clipblard.
                        HandleFinalTransaction(clipBoardStream, memStream, dataObject)
                        'resize the transactionGuids array as necessary and add the current transaction
                        'so next time we contact the web service we won't get this one again.
                        If (transactionGuids Is Nothing) Then
                            Array.Resize(transactionGuids, 1)
                        Else
                            Array.Resize(transactionGuids, (transactionGuids.Length + 1))
                        End If
                        transactionGuids((transactionGuids.Length - 1)) = clipBoardStream.TransactionID
                    End If
                Else
                    Exit While
                End If

            End While
            If (Not (clipBoardStream) Is Nothing) Then
                clipBoardStream.IsLastTransaction = False
                currentByte = 0
            End If

        End While
        Clipboard.SetDataObject(dataObject, True)
	

Conclusão

Nesse projeto, demonstramos como é possível serializar qualquer objeto (seqüência de caracteres, bitmap ou arquivo) da área de transferência em uma matriz de bytes para transmiti-lo com uma chamada a Web Service. Também demonstramos como armazenar em disco esses itens da área de transferência e recuperá-los posteriormente para remontagem na área de transferência de destino. Eu sempre achei que essa seria uma idéia interessante para um serviço hospedado/ASP (Application Service Provider, Provedor de Serviços de Aplicativo), no qual uma empresa seria o host do web service deste projeto para uso do cliente. Fique atento, pois em breve será lançado um artigo sobre como adicionar a funcionalidade da classe de associação do ASP .NET, fornecendo autenticação e permitindo que vários usuários utilizem a mesma instância do Web Service.

Para obter exemplos funcionais do aplicativo cliente e do Web Service em C# e VB faça este DOWNLOAD.

Biografia

Presidente e principal fundador da Personify Design, Brian supervisiona as operações de design e de desenvolvimento. Ele possui mais de 10 anos de experiência na indústria tecnológica. Em sua atual função, sua especialidade é desenvolver e planejar soluções completas para os clientes, que envolvem tecnologias de aplicativo da Web, como o SQL Server e o ASP .NET. Quando não está escrevendo códigos, Brian gosta de velejar em Puget Sound a bordo do Far Nienter, um veleiro Catalina MKII de 36 pés.