araujoarthur 0 Posted March 3 Hello y'all! I'm developing a Shopee Wrapper in Delphi and ran into a pitfall. I also ran into a similar problem that was answered on StackOverflow. I need to handle two kinds of asynchronous behavior that are needed for the main program (and the VCL visual part), but I don't know WHAT I should do for this. My issue starts with the following constructor noted in the code block (1). After the Authorize; call, any instruction dependent on Authorization Code falls into Undefined Behavior because there's no guarantee I have a code. I would need to wait for Authorize to return before following on. My issue arises from the fact that Authorize creates an object and starts a routine that is Asynchronous, a HTTP listener (as noted in code block) (2). The AuthorizationRequest procedure is implemented as shown and so does FieldsReadyHandler (which is the subject of my older question) in the current version.(3, 4). I was told to post here to get a better guidance on how to learn this subject and overcome this obstacle. I would like to answer the question "Why is there a server that is created then destroyed after a simple request". That was a design choice I made (maybe a poor one, I'm open to change this) for the URI. I needed it to be local so I came with the idea of opening a simple http server, waiting for the Authorization redirect, catching the code and shop_id then freeing it. This process is necessary at least once every 365 days. Usually it's a matter of HOW to do that, but honestly at this point I'm thinking that I overcomplicated it and don't even know WHAT I should do to overcome this. Anyways, if I were to rewrite I can see my self falling in the same spot where my subsequent tasks depends on the async task to finish. I also tried to add an event to inform the Context that it already obtained the code but (my shot is) it raises access violation because of the way the object is released. Sorry I can't synthesize a TL;DR for this one, but if I had to index my questions the output would be: What is the best approach to this type of problem and how is it handled in pascal? In JS, for example, I would chain infinite .then() until the routine doesn't need the async part anymore. I also feel it is going to become a frequent pain if I don't learn to handle it now as requests are asynchronous. Thanks in advance. {1. The constructor } constructor TShopeeContext.Create(AMode, AAPIKey, APartnerID: string); begin if (AMode <> TEST_SHOPEE) and (AMode <> PRODUCTION_SHOPEE_BR) then raise Exception.Create('Mode Invalid for Context'); FHost := AMode; FAPI_Key := AAPIKey; FPartnerID := APartnerID; UpdateInternalConfigFields; if FRefreshToken = '' then FHasRefreshToken := False else FHasRefreshToken := True; // Checks if the authorization has been granted and is younger than 365 days. if (not FAuthorizationGranted) or ((Now - FAuthorizationDate) > 365) then begin Authorize; end; end; { 2. Authorize Procedure } procedure TShopeeContext.Authorize; var Authorizator: TShopeeAuthorizator; begin // Request Authorization; Authorizator := TShopeeAuthorizator.Create(FHost, FAPI_Key, FPartnerID, 8342); try Authorizator.AuthorizationRequest; finally Authorizator.Free; end; end; { 3. AuthorizationRequest implementation } procedure TShopeeAuthorizator.AuthorizationRequest; begin // Obtem a assinatura da chamada FSignature := GetPublicSignString(API_PATH_AUTHORIZATION, FTimestamp); // Constroi os parametros FRequestParameters.AddItem('partner_id', FPartnerID); FRequestParameters.AddItem('timestamp', FTimeStamp); FRequestParameters.AddItem('sign', FSignature); FRequestParameters.AddItem('redirect', REDIRECT_URL_WS+':'+IntToStr(FPort)); ShellExecute(0, 'open', PChar(GenerateRequestURL(API_PATH_AUTHORIZATION, FRequestParameters)), nil, nil, SW_SHOWNORMAL); FCatcherServer := TCatcherServer.Create(); FCatcherServer.OnFieldsReady := FieldsReadyHandler; FCatcherServer.Listen(FPort); end; {4. FieldsReadyHandler implementation} procedure TShopeeAuthorizator.FieldsReadyHandler(Sender: TObject; Code, AuthorizatedID: string); begin // Handle Code, Auth Type and AuthorizatedID. FConfigurator := TConfiguratorFile.Create; try FConfigurator.SaveAuthorizationInfo(Code, AuthorizatedID, (Sender as TCatcherServer).AuthorizationType); FSuccess := True; finally FConfigurator.Free; end; TThread.Queue(nil, procedure begin Sender.Free; end); end; Share this post Link to post
mitch.terpak 5 Posted March 6 (edited) I don't completely understand your problem but I'm assuming the issue is the following. You're running something that has to be done on the background and you don't want your application to freeze up / be unresponsive until its done. Or you want to wait until a specific thing is done. https://docwiki.embarcadero.com/RADStudio/Athens/en/Using_TTask_from_the_Parallel_Programming_Library TTask.Run(procedure begin // Whatever async thing you want to run end); Let's say now you want to wait until its done, or check at least. var aTask : ITask; aTask := TTask.Run(procedure begin // Whatever async thing you want to run end); aTask.Wait(); What's important is that when you do something on VCL you do it on the main thread. The TTask.Run is always on a separate thread from the main. You'll have to use https://docwiki.embarcadero.com/Libraries/Alexandria/en/System.Classes.TThread.Synchronize to call the main thread and do VCL things from an async thread. aTask.Wait might achieve the opposite of what you want: Locking the main thread. If for some reason you have to use it elsewhere there's also a TTaskStatus that you could check. Or you can make another TTask.Run and put aTask.Wait inside it. Hope this helps! Edited March 6 by mitch.terpak Share this post Link to post
Dalija Prasnikar 1396 Posted March 6 It was my comment about the server creation which seemed unnecessary. This was a wrong assumption, and the server is needed for the authorization process. However, there are still some things in your code that could be improved. I will start with simple things. You are have plenty of unnecessary nil checks. If you want to return a nil or some field instance if it is assigned, then you can just return the value in the field as if the field is not assigned its default value will be nil. (Note, when it comes to local variables, they must be explicitly initialized to nil, as their default value will be random, unlike for object fields) So following code can be replaced function TServer.ServerLog: TStringList; begin Result := Nil; if Assigned(FServerLog) then begin Result := FServerLog; end; end; with function TServer.ServerLog: TStringList; begin Result := FServerLog; end; Next, you are constructing FServerLog in the constructor, so the whole nil check is pointless anyway because during the server instance lifetime it will be valid, assigned instance. In cases where it would not be a valid instance. If you would have a case where some function could return nil instance, you would have to always check for nil, before using such object. Free method can be safely called on nil instances, so you don't have to check if instance is assigned before calling Free. Following would be fine: destructor TServer.Destroy; begin FServer.Free; FServerLog.Free; inherited; end; Because server uses server log, I would also reverse order of construction and destruction and create server log first, and then the server, also server would be destroyed first, and then server log. You have similar code in other places and it can be simplified, too. Now, back to the actual problem you have been asking. First of all, TShopeeAuthorizator is inherently connected with TCatcherServer. It would make sense to create server in TShopeeAuthorizator constructor and destroy it in its destructor. This would also prevent memory leak you have if the FieldsReadyHandler is never called. This would also remove rather ugly Sender.Free code. It is not that it cannot be used in that way, but commonly such code can be replaced with better constructs. Next it would be better to move parts of the TShopeeContext.Authorize inside the TShopeeAuthorizator. This would also remove the need to have global AuhorizationDone event. Make it a field in TShopeeAuthorizator and create it in its constructor, and destroy in the destructor. The whole waiting for even loop will then be moved inside TShopeeAuthorizator.AuthorizationRequest which would be converted to function returning Boolean to detect successful authorization. procedure TShopeeContext.Authorize; var Authorizator: TShopeeAuthorizator; begin // Request Authorization; Authorizator := TShopeeAuthorizator.Create(FDataHolder, FHost, FAPI_Key, FPartnerID, 8342); try if Authorizator.AuthorizationRequest(45000) then begin ShowMessage('Autenticado com Sucesso'); ShowMessage(FDataHolder.Code + ' ' + FDataHolder.EntityID); end else raise Exception.Create('Authorization Timed Out'); finally Authorizator.Free; end; end; Additionally, you can also make TShopeeContext.Auhorize a Boolean function that would merely return the value returned by AuthorizationRequest. This would also move error handling to the higher level code which can then have better control of how to handle success and failure. You can also add event handlers FOnSuccess and FOnFailure to TShopeeContext so the outside code can hook to them. In such case Authorize does not need to be a function (but you can still leave it like that, so your code would have more flexibility. TShopeeContext = class private FOnAuhorizeSucess, FOnAuthorizeFailure: TNotifyEvent; ... published property OnAuthorizeSucess: TNotifyEvent read FOnAuhorizeSucess write FOnAuhorizeSucess; property OnAuthorizeFailure: TNotifyEvent read FOnAuthorizeFailure write FOnAuthorizeFailure; end; function TShopeeContext.Authorize: Boolean; var Authorizator: TShopeeAuthorizator; begin Authorizator := TShopeeAuthorizator.Create(FDataHolder, FHost, FAPI_Key, FPartnerID, 8342); try Result := Authorizator.AuthorizationRequest(45000); if Result then begin if Assigned(FOnAuthorizeSucess) then FOnAuthorizeSucess(Self); end else begin if Assigned(FOnAuthorizeFailure) then FOnAuthorizeFailure(Self); end; finally Authorizator.Free; end; end; Of course, you can move both success and failure into single event handler, but then you would need to define handler that would have additional parameter. Share this post Link to post
Navid Madani 1 Posted June 26 I cannot tell what your code is doing for sure. However, your question is also general about how to tackle asynchronous programming in Object Pascal. You are calling procedures in your class that do not return any values, where a more functional approach might be better suited for your needs. Assuming the consumer is in the main thread that you do not want to lock, you have a couple of options: You could use a TThread descendent class and set its OnTerminate even to a TNotifyEvent in your main thread (i.e., a method with the procedure(Sender: TObject) signature) that will receive the TThread descendent before its destruction and allow you to extract the necessary data. Alternatively, you could use an IFuture<T> (System.Threading) and query its value when you are ready, but this can be a blocking call if your future result is not yet available. Share this post Link to post