Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/owncloud/client.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/LocalSocketClient.swift')
-rw-r--r--shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/LocalSocketClient.swift289
1 files changed, 289 insertions, 0 deletions
diff --git a/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/LocalSocketClient.swift b/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/LocalSocketClient.swift
new file mode 100644
index 000000000..c0f2a3acd
--- /dev/null
+++ b/shell_integration/MacOSX/OwnCloudFinderSync/FinderSyncExt/LocalSocketClient.swift
@@ -0,0 +1,289 @@
+//
+// LocalSocketClient.swift
+// FinderSyncExt
+//
+// Created by Erik Verbruggen <erik@verbruggen.consulting> on 04-11-21.
+//
+
+import Foundation
+import OSLog
+
+/// Process lines from the `LocalSocketClient`.
+@objc protocol LineProcessor {
+ func process(line: String);
+}
+
+/// Class handling the (asynchronous) communication with a server over a local (UNIX) socket.
+///
+/// The implementation uses a `DispatchQueue` and `DispatchSource`s to handle asynchronous communication and thread
+/// safety. All public/@objc function can be called from any thread/queue. The delegate that handles the
+/// line-decoding is **not invoked on the UI thread**, but the (random) thread associated with the `DispatchQueue`!
+/// If any UI work needs to be done, the class implementing the `LineProcessor` protocol should dispatch this work
+/// on the main queue (so the UI thread) itself!
+///
+/// Other than the `init(withSocketPath:, lineProcessor)` and the `start()` method, all work is done "on the dispatch
+/// queue". The `localSocketQueue` is a serial dispatch queue (so a maximum of 1, and only 1, task is run at any
+/// moment), which guarantees safe access to instance variables. Both `askOnSocket(_:, query:)` and
+/// `askForIcon(_:, isDirectory:)` will internally dispatch the work on the `DispatchQueue`.
+///
+/// Sending and receiving data to and from the socket, is handled by two `DispatchSource`s. These will run an event
+/// handler when data can be read from resp. written to the socket. These handlers will also be run on the
+/// `DispatchQueue`.
+class LocalSocketClient: NSObject {
+ let socketPath: String
+ let lineProcessor: LineProcessor
+
+ private var sock: Int32?
+ private var localSocketQueue = DispatchQueue.init(label: "localSocketQueue")
+ private var readSource: DispatchSourceRead?
+ private var writeSource: DispatchSourceWrite?
+ private var inBuffer = [UInt8]()
+ private var outBuffer = [UInt8]()
+
+ @objc var isConnected: Bool {
+ get {
+ sock != nil
+ }
+ }
+
+ @objc init(withSocketPath socketPath: String, lineProcessor: LineProcessor) {
+ self.socketPath = socketPath
+ self.lineProcessor = lineProcessor
+
+ super.init()
+
+ self.inBuffer.reserveCapacity(1000)
+ }
+
+ private func log(_ str: String, type logType: OSLogType) {
+ if #available(macOSApplicationExtension 11.0, *) {
+ // NOTE: when support for 10.* is dropped, make an instance variable instead of instantiating the `Logger`
+ // object every time.
+ Logger().log(level: logType, "\(str, privacy: .public)")
+ } else {
+ os_log("%@", type: logType, str)
+ }
+ }
+
+ // MARK: Socket management
+ @objc func start() {
+ guard !self.isConnected else { return }
+
+ var sa_un = sockaddr_un()
+
+ let socketPathByteCount = socketPath.utf8.count + 1; // add 1 for the NUL terminator char
+ let maxByteCount = MemoryLayout.size(ofValue: sa_un.sun_path)
+ guard socketPathByteCount < maxByteCount else {
+ log("Socket path '\(socketPath)' is too long: \(socketPathByteCount) is longer than \(maxByteCount)",
+ type: .error)
+ return
+ }
+
+ log("Opening local socket...", type: .debug)
+
+ self.sock = socket(AF_LOCAL, SOCK_STREAM, 0)
+ guard self.sock != -1 else {
+ self.log("Cannot open socket: \(self.strErr())", type: .error)
+ self.restart()
+ return
+ }
+
+ log("Local socket openned, now connecting to '\(self.socketPath)' ...", type: .debug)
+
+ sa_un.sun_family = UInt8(AF_LOCAL & 0xff)
+
+ let pathBytes = socketPath.utf8 + [0]
+ pathBytes.withUnsafeBytes { srcBuffer in
+ withUnsafeMutableBytes(of: &sa_un.sun_path) { dstPtr in
+ dstPtr.copyMemory(from: srcBuffer)
+ }
+ }
+
+ let connStatus = withUnsafePointer(to: &sa_un) { sa_unPtr in
+ // We are now allowed to mess with the raw pointer to `sa_un`, and cast it to a `sockaddr` pointer.
+ // This is basically a barrier before and after this closure, so that all writes have been done before by
+ // the compiler, and that subsequent reads do not read old values (because Swift can't see if `connect`
+ // messes with the memory for which it receives a raw pointer).
+ sa_unPtr.withMemoryRebound(to: sockaddr.self, capacity: 1) { saPtr in
+ connect(self.sock!, saPtr, socklen_t(MemoryLayout<sockaddr_un>.size))
+ }
+ }
+
+ guard connStatus != -1 else {
+ self.log("Cannot connect to '\(self.socketPath): \(self.strErr())", type: .error)
+ self.restart()
+ return
+ }
+
+ let flags = fcntl(self.sock!, F_GETFL, 0)
+ guard -1 != fcntl(self.sock!, F_SETFL, flags | O_NONBLOCK) else {
+ self.log("Cannot set socket to non-blocking mode: \(self.strErr())", type: .error)
+ self.restart()
+ return
+ }
+
+ log("We have a connection! Starting dispatch sources...", type: .debug)
+
+ self.readSource = DispatchSource.makeReadSource(fileDescriptor: self.sock!, queue: self.localSocketQueue)
+ self.readSource!.setEventHandler { self.readFromSocket() }
+ self.readSource!.setCancelHandler {
+ self.readSource = nil
+ self.closeConnection()
+ }
+
+ self.writeSource = DispatchSource.makeWriteSource(fileDescriptor: self.sock!, queue: self.localSocketQueue)
+ self.writeSource!.setEventHandler { self.writeToSocket() }
+ self.writeSource!.setCancelHandler {
+ self.writeSource = nil
+ self.closeConnection()
+ }
+ // The writeSource dispatch queue starts suspended; we will resume it when we have data to send (and suspend it
+ // again when our send buffer is empty).
+
+ log("Going live NOW.", type: .debug)
+
+ self.readSource!.resume()
+ self.askOnSocket("", query: "GET_STRINGS")
+ }
+
+ private func restart() {
+ self.closeConnection()
+
+ DispatchQueue.main.async {
+ Timer.scheduledTimer(withTimeInterval: 5, repeats: false, block: { _ in
+ self.start()
+ });
+ }
+ }
+
+ private func closeConnection() {
+ self.readSource?.cancel()
+ self.writeSource?.cancel()
+ self.readSource = nil
+ self.writeSource = nil
+ self.inBuffer.removeAll()
+ self.outBuffer.removeAll()
+ if let sock = self.sock {
+ close(sock)
+ self.sock = nil
+ }
+ }
+
+ private func strErr() -> String {
+ let err = errno // copy error code now, in case something else happens
+ return String(utf8String: strerror(err)) ?? "Unknown error code (\(err))"
+ }
+
+ // MARK: Write data to socket
+ @objc func askOnSocket(_ path: String, query verb: String) {
+ let line = "\(verb):\(path)\n"
+ self.localSocketQueue.async {
+ guard self.isConnected else {
+ // socket was closed while work was still scheduled on the queue
+ return
+ }
+
+ self.log("Sending line '\(line)", type: .debug)
+
+ let writeSourceIsSuspended = self.outBuffer.isEmpty
+ let uint8Data: [UInt8] = line.utf8 + []
+ self.outBuffer.append(contentsOf: uint8Data)
+
+ // Weird stuff happens when you call resume when the DispatchSource is already resumed, so: if we did NOT
+ // have any data in our output buffer before queueing more data, it must be suspended.
+ if writeSourceIsSuspended {
+ self.writeSource?.resume() // now we will get notified when we can write to the socket.
+ }
+ }
+ }
+
+ private func writeToSocket() {
+ guard self.isConnected else {
+ // socket was closed while work was still scheduled on the queue
+ return
+ }
+
+ guard !self.outBuffer.isEmpty else {
+ // the buffer is empty, suspend you-can-write-data notifications
+ self.writeSource!.suspend()
+ return
+ }
+
+ let totalAmountOfBytes = self.outBuffer.count
+ let bytesWritten = self.outBuffer.withUnsafeBytes { ptr in
+ write(self.sock!, ptr.baseAddress, totalAmountOfBytes)
+ }
+ if bytesWritten == 0 {
+ // we reached "end of file", which means that the socket was closed. So in that case:
+ self.restart()
+ } else if bytesWritten == -1 {
+ let err = errno // make a copy, because errno is often volatile
+ if err == EAGAIN || err == EWOULDBLOCK {
+ // no space free in the buffer on the OS side, we're done
+ } else {
+ self.log("Error writing to local socket: \(self.strErr())", type: .error)
+ self.restart()
+ }
+ } else if bytesWritten > 0 {
+ self.outBuffer.removeFirst(bytesWritten)
+ if self.outBuffer.isEmpty {
+ // the buffer is empty, suspend you-can-write-data notifications
+ self.writeSource!.suspend()
+ }
+ }
+ }
+
+ @objc func askForIcon(_ path: String, isDirectory: Bool) {
+ let verb = isDirectory ? "RETRIEVE_FOLDER_STATUS" : "RETRIEVE_FILE_STATUS"
+ self.askOnSocket(path, query: verb)
+ }
+
+ // MARK: Process data from socket
+ private func readFromSocket() {
+ guard self.isConnected else {
+ // socket was closed while work was still scheduled on the queue
+ return
+ }
+
+ let bufferLength = self.inBuffer.capacity / 2
+ var buffer = [UInt8].init(repeating: 0, count: bufferLength)
+
+ while true {
+ let bytesRead = buffer.withUnsafeMutableBytes { ptr in
+ read(self.sock!, ptr.baseAddress, bufferLength)
+ }
+ if bytesRead == 0 {
+ // we reached "end of file", which means that the socket was closed. So in that case:
+ self.restart()
+ return
+ } else if bytesRead == -1 {
+ if errno == EAGAIN {
+ return // no bytes available, and no error, so we're done
+ } else {
+ self.log("Error reading from local socket: \(self.strErr())", type: .error)
+ self.closeConnection()
+ return // we've closed the connection, we're done
+ }
+ } else {
+ self.inBuffer.append(contentsOf: buffer[0..<bytesRead])
+ self.processInBuffer()
+ // see if there is more to read: restart the loop
+ }
+ }
+ }
+
+ private func processInBuffer() {
+ let separator: UInt8 = 0xa // byte value for '\n'
+ while true {
+ if let pos = self.inBuffer.firstIndex(of: separator) {
+ self.inBuffer[pos] = 0 // add NUL terminator
+ let newLine = String(cString: &self.inBuffer)
+ self.inBuffer.removeFirst(pos + 1) // remove the line from the buffer, including the NUL terminator
+ self.lineProcessor.process(line: newLine)
+ } else {
+ // no separator, we're done
+ return;
+ }
+ }
+ }
+}