I'm practicing making a URLProtocol subclass. I'm using URLSessionStreamTask to do the reading & writing. When trying out the subclass, I got a time-out. I thought I messed up my reading routine, but adding logging showed I didn't get past the initial write!
Here's a shortened version of my subclass:
import Foundation
import LoggerAPI
class GopherUrlProtocol: URLProtocol {
enum Constants {
static let schemeName = "gopher"
static let defaultPort = 70
}
var innerSession: URLSession?
var innerTask: URLSessionStreamTask?
override class func canInit(with request: URLRequest) -> Bool { /*...*/ }
override class func canonicalRequest(for request: URLRequest) -> URLRequest { /*...*/ }
override func startLoading() {
Log.entry("Starting up a download")
defer { Log.exit("Started a download") }
precondition(innerSession == nil)
precondition(innerTask == nil)
innerSession = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: .current)
innerTask = innerSession?.streamTask(withHostName: (request.url?.host)!, port: request.url?.port ?? Constants.defaultPort)
innerTask!.resume()
downloadGopher()
}
override func stopLoading() {
Log.entry("Stopping a download")
defer { Log.exit("Stopped a download") }
innerTask?.cancel()
innerTask = nil
innerSession = nil
}
}
extension GopherUrlProtocol {
func downloadGopher() {
Log.entry("Doing a gopher download")
defer { Log.exit("Did a gopher download") }
guard let task = innerTask, let url = request.url, let path = URLComponents(url: url, resolvingAgainstBaseURL: false)?.path else { return }
let downloadAsText = determineTextDownload(path)
task.write(determineRetrievalKey(path).data(using: .isoLatin1)!, timeout: innerSession?.configuration.timeoutIntervalForRequest ?? 60) {
Log.entry("Responding to a write with error '\(String(describing: $0))'")
defer { Log.exit("Responded to a write") }
Log.info("Hi")
if let error = $0 {
self.client?.urlProtocol(self, didFailWithError: error)
return
}
var hasSentClientData = false
var endReadLoop = false
let aMinute: TimeInterval = 60
let bufferSize = 1024
let noData = Data()
var result = noData
while !endReadLoop {
task.readData(ofMinLength: 1, maxLength: bufferSize, timeout: self.innerSession?.configuration.timeoutIntervalForRequest ?? aMinute) { data, atEOF, error in
Log.entry("Responding to a read with data '\(String(describing: data))', at-EOF '\(atEOF)', and error '\(String(describing: error))'")
defer { Log.exit("Responded to a read") }
Log.info("Hello")
if let error = error {
self.client?.urlProtocol(self, didFailWithError: error)
endReadLoop = true
return
}
endReadLoop = atEOF
result.append(data ?? noData)
hasSentClientData = hasSentClientData || data != nil
}
}
if hasSentClientData {
self.client?.urlProtocol(self, didReceive: URLResponse(url: url, mimeType: downloadAsText ? "text/plain" : "application/octet-stream", expectedContentLength: result.count, textEncodingName: nil), cacheStoragePolicy: .notAllowed) // To-do: Update cache policy
self.client?.urlProtocol(self, didLoad: result)
}
}
}
}
extension GopherUrlProtocol: URLSessionStreamDelegate {}
And the log:
[2017-08-28T00:52:33.861-04:00] [ENTRY] [GopherUrlProtocol.swift:39 canInit(with:)] GopherUrlProtocol checks if it can 'init' gopher://gopher.floodgap.com/
[2017-08-28T00:52:33.863-04:00] [EXIT] [GopherUrlProtocol.swift:41 canInit(with:)] Returning true
[2017-08-28T00:52:33.863-04:00] [ENTRY] [GopherUrlProtocol.swift:39 canInit(with:)] GopherUrlProtocol checks if it can 'init' gopher://gopher.floodgap.com/
[2017-08-28T00:52:33.863-04:00] [EXIT] [GopherUrlProtocol.swift:41 canInit(with:)] Returning true
[2017-08-28T00:52:33.864-04:00] [ENTRY] [GopherUrlProtocol.swift:54 canonicalRequest(for:)] GopherUrlProtocol canonizes gopher://gopher.floodgap.com/
[2017-08-28T00:52:33.864-04:00] [EXIT] [GopherUrlProtocol.swift:56 canonicalRequest(for:)] Returning gopher://gopher.floodgap.com
[2017-08-28T00:52:33.867-04:00] [ENTRY] [GopherUrlProtocol.swift:82 startLoading()] Starting up a download
[2017-08-28T00:52:33.868-04:00] [ENTRY] [GopherUrlProtocol.swift:112 downloadGopher()] Doing a gopher download
[2017-08-28T00:52:33.868-04:00] [EXIT] [GopherUrlProtocol.swift:113 downloadGopher()] Did a gopher download
[2017-08-28T00:52:33.868-04:00] [EXIT] [GopherUrlProtocol.swift:83 startLoading()] Started a download
[2017-08-28T00:53:33.871-04:00] [ENTRY] [GopherUrlProtocol.swift:132 downloadGopher()] Responding to a write with error 'Optional(Error Domain=NSPOSIXErrorDomain Code=60 "Operation timed out" UserInfo={_kCFStreamErrorCodeKey=60, _kCFStreamErrorDomainKey=1})'
[2017-08-28T00:53:33.871-04:00] [INFO] [GopherUrlProtocol.swift:134 downloadGopher()] Hi
[2017-08-28T00:53:33.872-04:00] [EXIT] [GopherUrlProtocol.swift:133 downloadGopher()] Responded to a write
[2017-08-28T00:53:33.876-04:00] [ENTRY] [GopherUrlProtocol.swift:95 stopLoading()] Stopping a download
[2017-08-28T00:53:33.876-04:00] [ERROR] [main.swift:42 cget] Retrieval Error: Error Domain=NSURLErrorDomain Code=-1001 "The request timed out." UserInfo={NSUnderlyingError=0x100e01470 {Error Domain=kCFErrorDomainCFNetwork Code=-1001 "(null)" UserInfo={_kCFStreamErrorCodeKey=-2102, _kCFStreamErrorDomainKey=4}}, NSErrorFailingURLStringKey=gopher://gopher.floodgap.com, NSErrorFailingURLKey=gopher://gopher.floodgap.com, _kCFStreamErrorDomainKey=4, _kCFStreamErrorCodeKey=-2102, NSLocalizedDescription=The request timed out.}
[2017-08-28T00:53:33.876-04:00] [EXIT] [GopherUrlProtocol.swift:96 stopLoading()] Stopped a download
Program ended with exit code: 11
Strangely, the logging for the writing closure appears only sometimes. Maybe it's some kind of threading/timing issue. (Here, I ran the program twice.)
Am I using URLSessionStreamTask wrong? Or URLProtocol wrong? Or, although it's not HTTP, am I triggering ATS?
It looks like you're queueing up an insane number of read callbacks in a while loop and blocking the thread where the callback will actually run by never returning from the while loop.
That read call is asynchronous, which means that the callback inside will run later—probably much later. Thus, your "while not EOF" thing will block the calling thread, ensuring that it never returns to the top of the run loop to allow the callback to run, thus ensuring that the callback will never be able to set the eof flag to terminate the while loop. Essentially, you deadlocked the thread.
You should almost never call asynchronous methods in any sort of loop. Instead:
create a method whose sole purpose is to return that block/closure (perhaps getReadHandlerBlock).
Call the read method, passing the block returned from a call to getReadHandlerBlock.
Then, the bottom of the read handler block:
Check to see if you need to read more.
If so, call getReadHandlerBlock on a weak reference to self to obtain a reference to a read handler block.
Call the read method.
Hope that helps. (Note that I'm not saying that this is the only problem with the code; I haven't looked at it in too much detail. It was just the first thing that I noticed.)
I was about to ask this query on a different forum. In my steps, I mentioned that I send out the request line...
Line? With... endings?
[Look up RFC 1436, Section 2]
Oh, I calculate and send the retrieval string from the URL, but I forgot to cap off the request with a CR-LF. That gets the request through.
But now I'm getting a time-out on the read-back....
Related
Pardon me for beginner's question. I'm following a tutorial, it has the following snippet.
I don't understand the point of dispatch_async, if you execute the block self.webView... on the main queue on the main thread by calling dispatch_get_main_queue() anyway, why bother putting it inside dispatch_async?
Thanks
let url = NSURL(string: "http://www.stackoverflow.com")
let task = NSURLSession.sharedSession().dataTaskWithURL(url!) {
(data, response, error) in
if error == nil {
var urlContent = NSString(data: data, encoding: NSUTF8StringEncoding)
println(urlContent)
dispatch_async(dispatch_get_main_queue()) {
self.webView.loadHTMLString(urlContent!, baseURL: nil)
}
}
}
task.resume()
dispatch_async is used to execute block on the other queue. It needs 2 parameters, first is the queue that it should execute in, second the code block.
NSURLSession.sharedSession().dataTaskWithURL(url!){...}
The reason why they use dispatch_async in your code is that the ... code block will be executed in other queue (not in the main queue).
So if you want to execute self.webView.loadHTMLString(urlContent!, baseURL: nil) in the main queue, then you have to use dispatch_async(dispatch_get_main_queue()){...}.
I'm new to swift coding, so most of my code is "this is how I found it on the internet" status.
I'm trying to send a HTTP GET request and then work with it. I'm using NSURLConnection for this. I have 2 swift files in my Xcode project (it is a swift console project, not playground), one is main one and 2nd contains my class I would like to use for delegation:
import Foundation
class Remote: NSObject, NSURLConnectionDelegate {
var data = NSMutableData()
func connect() {
var url = NSURL(string: "https://www.google.com")
var request = NSURLRequest(URL: url!)
NSLog("Is%# main thread", NSThread.isMainThread() ? "" : " NOT");
var conn = NSURLConnection(request: request, delegate: self, startImmediately: false)
conn?.scheduleInRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
conn?.start()
sleep(2)
}
func connection(didReceiveResponse: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
println("didReceiveResponse")
}
func connection(connection: NSURLConnection!, didReceiveData conData: NSData!) {
println("didReceiveData")
}
func connectionDidFinishLoading(connection: NSURLConnection!) {
println("DidFinishLoading")
}
deinit {
println("deiniting")
}
}
Here is my main swift code:
import Foundation
var xxx = Remote()
xxx.connect()
sleep(10)
sleep(2)
When I set breakpoints on each println in delegate functions, they are never hit. Execution is done from main thread according to NSLog output.
In debugger I can see that data are sent and received over the network, but I never get any output. I've seen many similar questions here but nothing helped me.
What am I doing wrong?
As Martin R suggested I added correct NSRunLoop into my code right after starting NSURLConnection:
self.shouldKeepRunning = true
let theRL = NSRunLoop.currentRunLoop()
while self.shouldKeepRunning && theRL.runMode(NSDefaultRunLoopMode, beforeDate: NSDate(timeInterval: 1.0, sinceDate: NSDate())) { }
shouldKeepRunning is bool variable defined in my class and it set to false in delegate function connectionDidFinishLoading, so
In a swift 2 command line tool (main.swift), I have the following:
import Foundation
print("yay")
var request = HTTPTask()
request.GET("http://www.stackoverflow.com", parameters: nil, completionHandler: {(response: HTTPResponse) in
if let err = response.error {
print("error: \(err.localizedDescription)")
return //also notify app of failure as needed
}
if let data = response.responseObject as? NSData {
let str = NSString(data: data, encoding: NSUTF8StringEncoding)
print("response: \(str)") //prints the HTML of the page
}
})
The console shows 'yay' and then exits (Program ended with exit code: 0), seemingly without ever waiting for the request to complete. How would I prevent this from happening?
The code is using swiftHTTP
I think I might need an NSRunLoop but there is no swift example
Adding RunLoop.main.run() to the end of the file is one option. More info on another approach using a semaphore here
I realize this is an old question, but here is the solution I ended on. Using DispatchGroup.
let dispatchGroup = DispatchGroup()
for someItem in items {
dispatchGroup.enter()
doSomeAsyncWork(item: someItem) {
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: DispatchQueue.main) {
exit(EXIT_SUCCESS)
}
dispatchMain()
You can call dispatchMain() at the end of main. That runs the GCD main queue dispatcher and never returns so it will prevent the main thread from exiting. Then you just need to explicitly call exit() to exit the application when you are ready (otherwise the command line app will hang).
import Foundation
let url = URL(string:"http://www.stackoverflow.com")!
let dataTask = URLSession.shared.dataTask(with:url) { (data, response, error) in
// handle the network response
print("data=\(data)")
print("response=\(response)")
print("error=\(error)")
// explicitly exit the program after response is handled
exit(EXIT_SUCCESS)
}
dataTask.resume()
// Run GCD main dispatcher, this function never returns, call exit() elsewhere to quit the program or it will hang
dispatchMain()
Don't depend on timing.. You should try this
let sema = DispatchSemaphore(value: 0)
let url = URL(string: "https://upload.wikimedia.org/wikipedia/commons/4/4d/Cat_November_2010-1a.jpg")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
print("after image is downloaded")
// signals the process to continue
sema.signal()
}
task.resume()
// sets the process to wait
sema.wait()
If your need isn't something that requires "production level" code but some quick experiment or a tryout of a piece of code, you can do it like this :
SWIFT 3
//put at the end of your main file
RunLoop.main.run(until: Date(timeIntervalSinceNow: 15)) //will run your app for 15 seconds only
More info : https://stackoverflow.com/a/40870157/469614
Please note that you shouldn't rely on fixed execution time in your architecture.
Swift 4: RunLoop.main.run()
At the end of your file
// Step 1: Add isDone global flag
var isDone = false
// Step 2: Set isDone to true in callback
request.GET(...) {
...
isDone = true
}
// Step 3: Add waiting block at the end of code
while(!isDone) {
// run your code for 0.1 second
RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1))
}
I got a problem with OpenTok, I got a session of OTSession and I want to call the method signalWithType so I can send a chat message.
In the start I have
var session : OTSession?
And then in my method where I want to send chat message from textField I get the error 'Could not find memember 'signalWithType'
func textFieldShouldReturn(textField: UITextField) -> Bool {
self.view.endEditing(true)
let message = sendMessageField.text
sendMessageField.text = ""
var type = ""
var maybeError : OTError?
session?.signalWithType(type, string: message, connection: nil, error: maybeError)
if let error = maybeError {
println(error)
} else {
println("besked blev sendt")
}
return false
}
I can't find out why it says it as I pretty sure I got the right types and that.
I have not have other problems with calling methods from session..
I'm trying to make a messaging app in Parse, but I get this error when trying to upload a PFObject.
The error says:
2014-11-22 14:43:21.154 Parse demo[688:27950] Warning: A long-running operation is being executed on the main thread.
Break on warnBlockingOperationOnMainThread() to debug.
and my code is for the sender-button is:
#IBAction func sendButton(sender: AnyObject) {
var message = PFObject(className:"message")
message["message"] = send.text
message.save()
where send.text is just a text-box.
Any recommendations or ways to proceed would be highly appreciated.
Try this instead. then you save in a background blok
#IBAction func sendButton(sender: AnyObject) {
var message = PFObject(className:"message")
message["message"] = send.text
message.saveInBackgroundWithBlock {
(succeeded: Bool!, error: NSError!) -> Void in
if (error != nil) {
println("Save : \(error)")
}
else{
println("Success! with save")
}
}
}
This is not exactly an error, but more a hint, that it'll block the app because you are making a synchronous save call. Use one of the saveInBackground* methods instead and the save will take place asynchronously on a background job.