Tuesday, September 14, 2010

Apache HTTP Server

Introduction

Apache HTTP Server is a famous open source web server.

Installation

yum install httpd

Default Behavior

  1. Server instance name: httpd
  2. Configuration file:
    1. /etc/httpd/conf/httpd.conf
    2. /etc/httpd/conf/conf.d/*
  3. TCP Port: 80
  4. Log files:
    1. /var/log/httpd/access_log
    2. /var/log/httpd/error_log
  5. Working folder: /var/www/html

Configuration

Configuration: Log Level

Default log level for httpd is “warn”.  You may change the log level to debug for more detail log message when you encounter problem with configuration.

For example:

LogLevel debug

Configuration: Server Side Includes (SSI)

SSI (Server Side Includes) are directives that are placed in HTML pages, and evaluated on the server while the pages are being served. They let you add dynamically generated content to an existing HTML page, without having to serve the entire page via a CGI program, or other dynamic technology.

To enable SSI in Apache HTTP, add the following in conf file:

AddType text/html .shtml 
AddHandler server-parsed .shtml 

<Directory /data/www> 
    Options +Includes 
</Directory>

If the index file is a SSI file, you should add this line too:

DirectoryIndex index.html index.html.var index.shtml

Configuration: Virtual Host

The term Virtual Host refers to the practice of maintaining more than one server on one machine, as differentiated by their apparent hostname. For example, it is often desirable for companies sharing a web server to have their own domains, with web servers accessible as www.company1.com and www.company2.com, without requiring the user to know any extra path information.

In order not to mess up the httpd.conf file, we may store the virtual host configuration in a separate file. (eg: /etc/httpd/conf/httpd-vhosts.conf)

vi /etc/httpd/conf.d/httpd-vhosts.conf
NameVirtualHost *:80 

<Directory /data/www> 
    Options +Includes 
</Directory> 

<VirtualHost *:80> 
    DocumentRoot /data/www 
    ServerName www.company1.com 
    ScriptAlias /cgi-bin/ /data/www/cgi-bin/  
</VirtualHost>

Configuration: HTTPS – Secure HTTP

https is HTTP secure protocol.

Installation:

yum install mod_ssl

You may restart httpd service and verify if secure http service is up and running by:

# netstat -na |grep -i 443
tcp        0      0 :::443                      :::*                        LISTEN

The configuration file for mod_ssl is stored in /etc/httpd/conf.d/ssl.conf.

Remember to exclude port 443 in firewall if you want to publish the service.

Configuration: Basic Authentication

mod_auth_basic is the module for http basic authentication.

The following command will prompt for password and create an entry of user with md5 hashed password:

htpasswd -c password_file username

An example of using the basic password file to protect a folder:

<Location /private/>
  AuthType basic
  AuthName "private area"
  AuthBasicProvider file
  AuthUserFile conf.d/password_file
Require valid-user </Location>

Configuration: Digest Authentication

mod_auth_digest is the module for http digest authentication.  It has similar mechanism as basic authentication except the password sent as MD5 hash value instead of plain text.  httpd.conf should include mod_auth_digest by default.

The following command will prompt for password and create an entry of user with md5 hashed password:

htdigest -c password_file digest-realm username

An example of using the digest password file to protect a folder:

<Location /private/>
  AuthType Digest
  AuthName "digest_realm"
  AuthDigestProvider file
AuthUserFile conf.d/password_file Require valid-user </Location>

Configuration: httpd 2.2 LDAP Authentication and authorisation

httpd 2.2 using a module mod_authnz_ldap to perform ldap authentication and authoisation.

The following example attempt to protect a URL cgi-bin with username/password authentication against a ldap directory server.  The LDAP connection is using TLS.

<Location /cgi-bin>
        Order deny,allow
        Allow from example.com
        AuthType Basic
        AuthName "CVSweb"
        AuthBasicProvider ldap
        AuthzLDAPAuthoritative off
        AuthLDAPURL ldap://ldap.example.com/ou=user,dc=example,dc=com?uid?sub
?(objectclass=posixAccount) STARTTLS
        Require valid-user
</Location>

The above configuration should work if your LDAP server is working.

If debug log level is used, you may notice the error_log shows something like this:

[Tue Sep 14 16:38:18 2010] [debug] mod_authnz_ldap.c(376): [client 192.168.0.107] [32672] auth_ldap authenticate: using URL ldap://ldap.example.com/ou=example,dc=example,dc=com?uid?sub?(objectclass=posixAccount)
[Tue Sep 14 16:38:18 2010] [debug] mod_authnz_ldap.c(475): [client 192.168.0.107] [32672] auth_ldap authenticate: accepting alice
[Tue Sep 14 16:38:18 2010] [debug] mod_authnz_ldap.c(847): [client 192.168.0.107] [32672] auth_ldap authorise: authorisation denied

A log message of “authorisation denied” sounds like the authorisation fail.  However, you don’t encounter problem accessing resource from web browser.  This message may confuse when checking the log message during troubleshooting session.

The message was a result from a configuration option:

AuthzLDAPAuthoritative off

The httpd security mechanism requires authentication and authorisation processes to be verified before a resource may access by an user.  In this case, the user is authenticated with user name and password via LDAP service.  And the authorisation process is verified by apache configuration.

Setting AuthzLDAPAuthoritative to off means mod_authnz_ldap let other authorization modules attempt to authorize the user, should authorization with this module fail.  That is the reason why the message “authorisation denied” shown in log.  The mod_authnz_ldap denied to perform authorisation here due to “AuthzLDAPAuthoritative off”.

Now, next problem raise.  If mod_authnz_ldap_ldap denied to perform authorisation, why the resource is accessible after valid credential is supplied via web browser?  And why the authorisation seems successfully perform?  Who perform the authorisation?

The key is this setting:

Require valid-user

The above clause performs authorisation.  It simply means authenticated user of mod_authnz_ldap is always valid and is authorised to use the resource.

What if we attempt to set AuthzLDAPAuthoritative to on?  This simply means mod_authnz_ldap will perform authorisation followed by success authentication:

<Location /cgi-bin>
        Order deny,allow
        Allow from example.com
        AuthType Basic
        AuthName "CVSweb"
        AuthBasicProvider ldap
        AuthzLDAPAuthoritative on
        AuthLDAPURL ldap://ldap.example.com/ou=user,dc=example,dc=com?uid?sub?(objectclass=posixAccount) STARTTLS
        Require valid-user
</Location>

Above configuration will cause browser keep prompt for username and password even correct credential is provided.  Log messages something like these will shown:

[Tue Sep 14 16:30:49 2010] [debug] mod_authnz_ldap.c(376): [client 192.168.0.107] [32596] auth_ldap authenticate: using URL ldap://ldap.example.com/ou=user,dc=example,dc=com?uid?sub?(objectclass=posixAccount)
[Tue Sep 14 16:30:50 2010] [debug] mod_authnz_ldap.c(475): [client 192.168.0.107] [32596] auth_ldap authenticate: accepting alice
[Tue Sep 14 16:30:50 2010] [debug] mod_authnz_ldap.c(842): [client 192.168.0.107] [32596] auth_ldap authorise: declining to authorise

The error message is “declining to authorise” as compare to “authorisation denied” in previous case.

In this case, mod_authnz_ldap should attempt to perform authorisation due to “AuthzLDAPAuthoritative  on” clause in configuration.  Again, the reason it “declining to authorise” was due to this clause:

Require valid-user

In this case, mod_authnz_ldap should perform authorisation but valid-user.  However, mod_authnz_ldap do not possess “Require valid-user” and thus it doesn’t know know how to perform authorisation and that lead to “declining to authorise” message logged.

To make mod_authnz_ldap perform authorisation successfully, we may use either:

  1. Require ldap-user
  2. Require ldap-group
  3. Require ldap-dn
  4. Require ldap-attribute
  5. Require ldap-filter

for different cases.  Refer here for more information.

Configuration: WebDAV

mod_dav is the module for HTTPD WebDAV.  it is extremely easy to use WebDAV in Apache HTTPD:

<Location /foo>
  Dav On
</Location>

Just include “Dav On” to Location or Directory block will straight turn on WebDAV share.

Reference

  1. Making Apache 2.2 valid-user work with mod_authnz_ldap.
    URL: http://neptune.ashtech.net/~dmarkle/blog/archives/108-Making-Apache-2.2-valid-user-work-with-mod_authnz_ldap.html

Thursday, September 09, 2010

CVS: Web GUI by FreeBSD CVSweb

Introduction

CVSweb is a WWW interface for CVS repositories with which you can browse a file hierarchy on your browser to view each file's revision history in a very handy manner.

Installation

# Web interface for CVS repositories 
yum install cvsweb

# Runtime Logging for C++ 
yum install rlog

# Revision Control System (RCS) file version management tools 
yum install rcs

# CVS/RCS repository grapher 
yum install cvsgraph

# A plain ASCII to PostScript converter 
yum install enscript

Configuration: Repository

Update the repository setting in CVSweb configuration file:

vi /etc/cvsweb/cvsweb.conf
@CVSrepositories = (
      'local'   => ['Local Repository', '/var/cvs'],
#       'freebsd' => ['FreeBSD',          '/var/ncvs'],
#       'openbsd' => ['OpenBSD',          '/var/ncvs'],
#       'netbsd'  => ['NetBSD',           '/var/ncvs'],
#       'ruby'    => ['Ruby',             '/var/anoncvs/ruby'],
);

Configuration: Web Server

CVSweb is a cgi program runs on Apache, the Apache configuration should configuration similar to this:

ScriptAlias /cgi-bin/ "/var/www/cgi-bin/"

Make sure the CVSweb cgi is place in the path specify in ScriptAlias.

Configuration: Syntax Highlighter

The syntax highlighter works if enscript package is installed. CVSweb keep the enscript configuration in /etc/cvsweb/cvsweb.conf.

The following example specify Delphi as syntax highlighter for pascal (.pas) file.

vi /etc/cvsweb/cvsweb.conf
%enscript_types =
(
 'ada'          => qr/\.ad(s|b|a)$/o,
 'asm'          => qr/\.[Ss]$/o,
 'awk'          => qr/\.awk$/o,
 'bash'         => qr/\.(bash(_profile|rc)|inputrc)$/o,
 'c'            => qr/\.(c|h)$/o,
 'changelog'    => qr/^changelog$/io,
 'cpp'          => qr/\.(c\+\+|C|H|cpp|cc|cxx)$/o,
 'csh'          => qr/\.(csh(rc)?|log(in|out)|history)$/o,
 'elisp'        => qr/\.e(l|macs)$/o,
 'fortran'      => qr/\.[fF]$/o,
 'haskell'      => qr/\.(l?h|l?g)s$/o,
 'html'         => qr/\.x?html?$/o,
 'idl'          => qr/\.idl$/o,
 'inf'          => qr/\.inf$/io,
 'java'         => qr/\.java$/o,
 'javascript'   => qr/\.(js|pac)$/o,
 'ksh'          => qr/\.ksh$/o,
 'm4'           => qr/\.m4$/o,
 'makefile'     => qr/(GNU)?[Mm]akefile(?!\.PL\b)|\.(ma?ke?|am)$/o,
 'matlab'       => qr/\.m$/o,
 'nroff'        => qr/\.man$/o,
 'delphi'       => qr/\.p(as|p)?$/io,
 'perl'         => qr/\.p(m|(er)?l)$/io,
 'postscript'   => qr/\.e?ps$/io,
 'python'       => qr/\.py$/o,
 'rfc'          => qr/\b((rfc|draft)\..*\.txt)$/o,
 'scheme'       => qr/\.(scm|scheme)$/o,
 'sh'           => qr/\.sh$/o,
 'skill'        => qr/\.il$/o,
 'sql'          => qr/\.sql$/o,
 'states'       => qr/\.st$/o,
 'synopsys'     => qr/\.s(cr|yn(th)?)$/o,
 'tcl'          => qr/\.tcl$/o,
 'tcsh'         => qr/\.tcshrc$/o,
 'tex'          => qr/\.tex$/o,
 'vba'          => qr/\.vba$/o,
 'verilog'      => qr/\.(v|vh)$/o,
 'vhdl'         => qr/\.vhdl?$/o,
 'vrml'         => qr/\.wrl$/o,
 'wmlscript'    => qr/\.wmls(cript)?$/o,
 'zsh'          => qr/\.(zsh(env|rc)|z(profile|log(in|out)))$/o,
);

Using CVSweb

Launch your favorite web browser and navigate to

http://<cvs-host>/cgi-bin/cvsweb.cgi

Troubleshoot: CVS folders doesn’t show up

The CVS folder specify in local repository of cvsweb.conf may not have appropriate access permission for cvsweb.cgi. You may use the following command to change the folder permission:

chmod o+rx <cvs-folder>

Troubleshoot: Encounter permission denied when browse a CVS tree

A CVS repository tree may contain arbitrary levels of folder. The tree may not have appropriate access permission for cvsweb.cgi. You may use the following command to change all the sub folders found in CVS repository:

find . -type d -exec chmod o+rx '{}' \;

Reference

  1. FreeBDS CVSweb Project. URL: http://www.freebsd.org/projects/cvsweb.html

Wednesday, September 08, 2010

CVS: Branching, Merging and Tagging

Introduction

CVS a.k.a. concurrent version system is the software version control in the field of software development.  Branching and Merging features in CVS allow developer to manage same piece of source file for several release and develop these release in parallel without interrupting each others.

Please note that the examples shown below are using WinCVS 1.2.

CVS Branching

For example, I have a file in CVS repository: App.DIY.Reg.pas.  The latest version for the file is 1.2 (The graph view is triggered with Ctrl+G Graph Selection):

1

Let look at the following scenario to see how could CVS branching manage the case:

I would like to make changes to a file.  These new changes is not yet ready for release to public. It may stay in CVS repository as internal or beta version for some time.  It will merge with main branch of the source file until it is mature.  However, the main branch may change due to bugs reported during the beta release period.  The bug fixed in main branch may merge with sub branch too during the beta release period.

The main trunk of a file in CVS is termed as “HEAD”.  You should define a meaningful name or tag to other branches but not the “HEAD” tag.  “HEAD” a reserve tag for main trunk.

Let us make some changes to local working versio of the file.  It will turn to red color once I make some changes:

2

The new changes is not ready for main release yet.  I am going to create a branch call BETA in version 1.2.  Please note that branch tag is case sensitive.

There are 2 ways to create a branch:

  1. Access via main menu.  Modify | Create a branch on selection... :

    4
  2. Click the Fork Selection of Tags tool bar icon:

    3

A windows with title “Create branch settings” prompt out:

5

Enter branch name and press OK button to create a “BETA” branch for the file.  We will leave an option “Check that the files are unmodified before branching” unchecked in this case.

The version of the file in graph view has a BETA branch shown:

6

Now everything seems ready and we are going to commit the local changes to BETA branch.  We will in trouble if we commit the changes now.  The changes we commit will stay in main trunk as version 1.3:

8

This is due to the sticky tag for the file in local copy is not mark as “BETA” tag.  It simply means we are working with the copy of main trunk in our local repository.  Whatever changes we commit will stay with main trunk:

7

We may update our local copy stick with "BETA” tag with CVS Update Selection:

9

Enter the stick tag “BETA” and press OK button to update your copy of file as BETA.

A

You will notice there is a tag BETA stick to the file after Update Selection.  Commit the local changes now and it will shown in BETA branch.  Please note also the file revision will update to 1.2.2.1 in this case:

B

We may remove the sticky tag and back to main trunk by checking an option “Reset any sticky date/tag/’-k' options” in Update Selection:

C

The local copy will become version 1.2 in this case:

D

We may always switch between HEAD and BETA branch by:

  1. “Reset any sticky date/tag/’-k' options” in Update Selection
  2. Retrieve rev./tag/branch of Stick options in Update Selection

respectively.

Now, assume we have another local working copy of the source with version 1.2 and empty sticky tag (a.k.a main trunk).  Perform Update Selection to this file will remain as 1.2.  It won’t update to 1.2.2.1 of BETA branch. unless we update the sticky tag to BETA.  We may continue making changes to version 1.2 and commit it as version 1.3, 1.4 or 1.5 and so:

E

CVS Merging

Let us back to version 1.2.2.1 of BETA branch.  We decided to merge the changes of 1.5 in main trunk to 1.2.2.1 of BETA branch.

Use the “Merge options” of Update Selection to merge main trunk into BETA branch:

F

Press OK button to proceed the merge operation.  Your local copy will then merge with main trunk.  You may decide to commit under BETA branch.  The following graph shows the result of commit work:

G

Now we decide to end the BETA branch and merge all changes from BETA branch to main trunk.  Let’s back to main trunk copy and perform the merging work:

H

After commit the changes, CVS repository will have the following version:

I

The version 1.6 is result of the merge of 1.5 in main trunk and 1.2.2.2 BETA branch.

CVS Tagging

CVS tagging allows you to mark an indication to a file for future reference.  A common usage for tagging is tag the source files with release or build number for future reference.  You may check out particular tag of source files for debugging purpose in future.

Here, we continue with CVS branching and merging example to use the CVS tagging feature to mark both 1.2.2.2 and 1.6 as “BETA_END”  and “BETA_MERGE” respectively.

To tag a version, use either:

  1. Access via main menu.  Modify | Create a tag on selection... :

    M
  2. Click the Tag Selection of Tags tool bar icon:

    L

A windows with title “Create tag settings” prompt out:

K

The following graph show the result of tagging:

J

The tagging is for reference only.  Remember that version 1.2.2.2 still exist in the CVS repository.  We may always go back to it anytime.

Working with main trunk and branch together in difference folders

When we start using branch in CVS repository, there may be a need to work with main trunk and difference branches together in same machine.  Check out a local copy from main trunk is easy:  Just check out as usual without any sticky tag.

Check out a local copy from branch may need extra care.  In real world example, only some of the source files may have branch.  Majority of source files may not have branch or already merge to main trunk.  If we check out a module with sticky options:

N

You will only get files with BETA tag:

O

This is not what we want in most situation.  We still need other files from main trunk in order to build a complete BETA release.  To check out the files from main trunk, check the option “If no matching revision is found, use the most recent one” :

P

Press the OK button and the local source copy will have:

Q

The same usage is also applicable to Update Selection.

Now, you will see all files in your local copy have BETA sticky tag.  However, this doesn’t mean all files have BETA branch in CVS repository.  In the example, only App.DIY.Reg.pas has BETA branch and the rest are not.  This always confuse the CVS user.

Now, if you make changes to a file App.DIY.pas and you decide to put it in BETA branch.  If you attempt to commit the local changes in this example, you will encounter:

cvs commit: file `App.DIY.pas' had a conflict and has not been modified
cvs [commit aborted]: correct above errors first!

This is due to App.DIY.pas doesn’t has BETA branch in CVS repository, you have to create a new BETA branch for it first before you can commit.  Refer to CVS Branching in early section of this article.

Friday, July 23, 2010

A machine equipped with 4 Gig RAM is so lagging running Linux PAE kernel and Windows 7 x64

I have a machine equipped with Intel motherboard DG965RY that has 4G RAM.  The machines work as usual running Windows Vista for years.

I installed Windows 7 x64 on the machine recently and the OS works extremely slow.  The CPU is Intel(R) Core(TM)2 CPU 6600 @ 2.40Ghz.  It should support 64 bits instruction sets as seens in BIOS page showing EM64T.  I first thought it could be CPU or motherboard that doesn’t support x64 OS well.  I then re-install the machine with Windows 7 x86 and it works like a charm.

2 Days later, I try to install AsteriskNOW backed by CentOS 5.5 (kernel 2.6.18-194.3.1.e15PAE) i386 on the same machine.  Again, the machine running the kernel 2.6.18-194.3.1.el5PAE is so lagging.  However, it works as usual if running kernel 2.6.18-194.3.1.el5.

I google for the different between PAE kernel and non PAE kernel.  PAE stand for Physical Address Extension.  A clue sparks on my mind suddenly after knowing from the PAE term that it might be something to do with the memory.  I remember that 32 bits machine have limitation on memory address in Windows Vista x86.  It may only detect 3.5G from 4G RAM installed.

I unplug a 2G RAM from motherboard slot and left 2G RAM to the machine and attempt to run PAE kernel.  It works like a charm.  A good news follow is Windows 7 x64 works extremely smooth with this 3 years old machine too.

I quickly browse the Intel BIOS update page and found there is update regarding addressing issues of 4G RAM.  I download and update the BIOS to the latest version, plug the 2G RAM back to motherboard and boot the machine with 4G RAM.  Both Linux PAE kernel and Windows 7 x64 works smoothly as expected.

Google Chrome is laging running on Windows 7 x64

Google Chrome is a web browser released by Google.  Google claims that the Chrome is the fastest browser in the market.  However, this is not my first experience when running Google Chrome on Windows 7 x64 desktop.

When I use Chrome on my Windows 7 x64 desktop, it will so lag and slow.  I continued using FireFox for quite some time and I am happy with the performance on my new Intel I7 machine equipped with 6G RAM.  I use Chrome when I want to use “Incognito Window” of Chrome to access my bank account.  It always lag for each page I visit.  This is not a pleasant experience with Chrome.

After google for the reason why Chrome is so lag for some time, I finally found a solution.  Uncheck “Automatically detect settings” in Internet Options of Microsoft Internet Explorer will boost the browsing speed of Chrome running on Windows 7 x64 platform.  Reason remain unknown why this helps.  No explanation from Google Chrome team so far.

To access Internet Options, you may use Tools | Options of Internet Explorer or double click the icon of Internet Options in Control Panel:

  1. Switch to Connections page
  2. Click “LAN Setting…” button to show “Local Area Network (LAN) Settings” dialog
  3. Uncheck “Automatically detect settings”
  4. Click OK button of LocalArea Network (LAN) Setttings dialog
  5. Click OK button of Internet Options dialog

Capture

You may then enjoy using Google Chrome to surf Internet in much responsive speed on Windows 7 x64 platform.  A side effect of this is the Internet Explorer 8 also render web pages faster than before.

Friday, May 21, 2010

Linux: Configure DHCP Server

It is pretty easy to configure DHCP server for Linux.

Install DHCP server

# yum list dhcp* 
Installed Packages 
dhcp.i386                                12:3.0.6-10.fc8        installed 
dhcpv6-client.i386                       0.10-51.fc8            installed 
Available Packages 
dhcp-devel.i386                          12:3.0.6-10.fc8        fedora 
dhcp-forwarder.i386                      0.7-12.fc7             fedora 
dhcp-forwarder-sysv.i386                 0.7-12.fc7             fedora 
dhcp-static.i386                         12:3.0.6-10.fc8        fedora 
dhcpv6.i386                              0.10-51.fc8            fedora

# yum install dhcp.i386  // depend on the yum list output

Configure DHCP Server

A sample dhcpd.conf can be found in /usr/share/doc/dhcpd-*/dhcpd.conf.sample  You may grab it and copy to /etc folder for further configuration.

Prior to Fedora 11, the dhcpd.conf is keep under folder /etc.

Fedora 11 and above, the dhcpd.conf should keep in folder /etc/dhcp

Wednesday, April 07, 2010

ANT: Java Heap Space Error

From time to time, when we use ANT script to perform jobs that drag some how longer time.  We may encounter Java heap space out of memory error.

The default heap space allocated for Java heap could be 128MB.  You may adjust the Java heap size from ANT via environment variable ANT_OPTS:

set ANT_OPTS=–Xmx512m

You may try to set in the system environment variable that is available to all users.  If it fail with message “Incompatible minimum and maximum heap sizes specified”, try remove the ANT_OPTS in system environment variable and set it in your home user account.

Tuesday, March 23, 2010

Share a folder in Windows 7

Sharing a folder of Windows 7 file system is as easy as Windows XP.  The following guidance shows how to share a folder in Windows 7 step by step.

The example shows how to share a folder of c:\estream\share.

  1. Open Windows Explorer and attempt to locate folder c:\estream\share
  2. Click the “Share with” on tool bar to bring out the sharing popup menu
  3. Click “Nobody” item

1

A File Sharing window show.  Choose “Changing sharing permissions”

2

Next window will show out.  The example shows how to share the folder for everyone’s access.  You may specify user name for limited access:

  1. Click the combo box to select a user name. e.g.: Everyone
  2. Click “Add” button to add the user
  3. Specify the "Level” of access. e.g.: Read
  4. Click “Share” button to commit (require privilege access)

3

Done.  You have successfully share the folder from Windows 7.  Go to any workstation to check if you may view the share folder via UNC path (e.g.: \\server)

Rename the share name

You may also use different name for the share folder:

  1. Right click the share folder to show the popup menu
  2. Click Properties to the following window
  3. Switch to “Sharing” page
  4. Click “Advanced Sharing” (require privilege access)

4

Next, “Advanced Sharing” window shows.  Click Add button to enter new share name.  You may remove old share name by clicking the “Remove” button.

5

Enter new share name and press OK button

6

Done.  You may check from other networked workstation if the new share name is up and ready to use.

Sunday, February 21, 2010

When not to use DoubleBuffered

As according to Delphi’s help,

DoubleBuffered determines whether the control's image is rendered directly to the window or painted to an in-memory bitmap first.

When DoubleBuffered is false, the windowed control paints itself directly to the window. When DoubleBuffered is true, the windowed control paints itself to an in-memory bitmap that is then used to paint the window. Double buffering reduces the amount of flicker when the control repaints, but is more memory intensive.

However, not all VCL’s controls work nice with DoubleBuffered.  There are at least 2 controls that I am aware of that can’t work with DoubleBuffered and would mess up visually.

These two controls are TRichEdit and TToolBar.  Any controls that are family of these 2 controls will mostly show expected outcome if using with DoubleBuffered.

You would probably not able to type text visually with TRichEdit if DoubleBuffered is True.  For TToolBar, it would show black background.  However, you may always place TToolBar in TPanel control to get rid of the DoubleBuffered nightmare.

Reference:

  1. Delphi on line help topic: Controls.TWinControl.DoubleBuffered. URL: http://docwiki.embarcadero.com/VCL/en/Controls.TWinControl.DoubleBuffered

Wednesday, December 02, 2009

TxQuery 2.0

TxQuery is created and owned by Alfonso Moreno.  He has stopped the development of TxQuery for years.  However, there are many Delphi developers still using TxQuery.  A barrier for TxQuery migrating to Delphi 2009 and 2010 is the introduction of Unicode.

I contact Alfonso Moreno on Nov 17, 2009 to ask if he may consider make TxQuery open source and let this great product continue enhanced by the Delphi community.  He finally agree and I wish to say big "Thank You" for his contribution.

I have attempted to patch the source code to make it compile and work with Delphi Unicode.  Test cases has been created to make sure it works as expected.  I know there are other cases that I didn't cover yet, just alert me promptly.  You are also welcome to join the maintenance and enhancement for this project.

TxQuery component is a TDataSet descendant component that can be used to query one or more TDataSet descendant components using SQL statements. It is implemented in Delphi 100% source code, no DLL required, because it implements its own SQL syntax parser and SQL engine.

It can be very useful for TDataSet descendants components (including TClientDataSet) that do not use the BDE and that do not implement the SQL language or to mix tables types (dbase, paradox, access).

TxQuery Project Page: https://code.google.com/p/txquery/
TxQuery Group: http://groups.google.com/group/txquery

Saturday, November 07, 2009

Firebird: Deal with Unavailable Database

“unavailble database” error prompt connecting to Firebird service is annoying and difficult to trace.  Sometime it work and sometime it doesn’t.  We don’t know when it work and when it doesn’t work.

“unavailable database” happen in TCP/IP connection

Use TCP/IP connection string to connect to a Firebird database is easy to trace.  The connection string looks like

  • 192.168.5.1:/data/db/test.fdb
  • localhost:/data/db/test.fdb

Please note that localhost is not using local protocol connection but it is a TCP/IP connection using loopback address.

If you encounter “unavailable database” error for TCP/IP connection, please make sure:

  1. You network connection on both end are working and the TCP/IP configuration is correct.  Try to run “PING” to the Firebird server will confirm the connection is working.
  2. Make sure Firebird Service is running
  3. Make sure Firewall doesn’t block Firebird listening port.  Default port number is 3050
  4. Make sure the connection string point to valid Firebird database file

Once you got above verified, the “unavailable database” error prompt should gone.

“unavailable database” happen in Local Protocol

The data transmission throughput for local protocol is few times better than TCP/IP connection.  However, it is easy to encounter “unavailable database” error while using Local Protocol when Firebird is running as service.  The connection string is simple:

  • c:\db\test1.fdb
  • c:\db\test2.fdb

The connection string is the database file path itself without any prefix.

Using Local Protocol with Firebird 1.5

  Run as Application Run as Service
Windows XP Yes Yes. Only work for first logon user
Windows Vista Yes No

Using Local Protocol with Firebird 2.X

  Run as Application Run as Service
Windows XP Yes Yes.  Work for any number of logon users
Windows Vista Yes No

Please note that Local Protocol only works for:

  1. x86 OS and x86 Firebird
  2. x64 OS and x64 Firebird

Local Protocol doesn’t work for x64 OS and x86 Firebird.

The above tables explains why local protocol sometime work and sometime doesn’t work.

Monday, October 19, 2009

DataSnap: In-process IAppServer connection via TDSProviderConnection

Classic DataSnap

Prior to Delphi 2009, we may use either TLocalConnection or TSocketConnection together with TConnectionBroker for in-process or out-of-process communication via IAppServer interface.  There are even more DataSnap connection that supports IAppServer.  Check Delphi helps for details.

New DataSnap from Delphi 2009

Previously, TSQLConnection was used in DataSnap server only.  In new DataSnap, we may use TSQLConnection in DataSnap client.  There is a new driver call DataSnap that allow us to connect to a DataSnap server either via TCP or HTTP protocol using REST data packet for multi-tier application.  Furthermore, we may use connect to TDSSever (TDSServer.Name) via TSQLConnection.DriverName for in-process connection.  This benefits us to write a scalable multi-tier DataSnap application to consume server methods.  See here for more details.

In Delphi 2009/2010, a new DataSnap connection component – TDSProviderConnection was introduced.  As the name implied, it supply providers from DataSnap server.  This connection require a TSQLConnection instance to work with in client tier.  Thus, we may use a single TSQLConnection in client tier either in-process or out-of-process.  And that fulfill the design philosophy of  scalable multi-tier DataSnap application.

There are lots of demo or CodeRage videos available in the web showing how to TDSProviderConnection in DataSnap client tier.  However, most of the examples only showing out-of-process design.  I never find one example illustrate the usage of TDSProviderConnection for in-process design while writing this topic.  Hope there are more from other famous or well know Delphi fans.

At first, I thought it is easy to use TDSProviderConnection for in-process design.  But I face problems while follow the rules.  These problems should be related to bugs and in mature design of DataSnap framework.  I will show at here how to deals with the problems

Design a DataSnap module

First, we design a simple DataSnap module for this example.  This is a TDSServerModule descendant instance with 2 components: a TDataSetProvider and a TClientDataSet instance.  The reason using TDSServerModule is it will manage providers define in the module.

MySeverProvider.DFM

object ServerProvider: TServerProvider
  OldCreateOrder = False
  OnCreate = DSServerModuleCreate
  Height = 225
  Width = 474
  object DataSetProvider1: TDataSetProvider
    DataSet = ClientDataSet1
    Left = 88
    Top = 56
  end
  object ClientDataSet1: TClientDataSet
    Aggregates = <>
    Params = <>
    Left = 200
    Top = 56
  end
end

MyServerProvider.PAS

type
  TServerProvider = class(TDSServerModule)
    DataSetProvider1: TDataSetProvider;
    ClientDataSet1: TClientDataSet;
    procedure DSServerModuleCreate(Sender: TObject);
  end;

{$R *.dfm}

procedure TServerProvider.DSServerModuleCreate(Sender: TObject);
begin
  ClientDataSet1.LoadFromFile('..\orders.cds');
end;

Define a transport layer for the provider module

Since this is a in-process application, we don’t really need a physical transport layer for the provider module.  What we need here is a TDSServer and a TDSServerClass instance that helps propagate the providers to ClientDataSet in later stage.

var C: TDSServer:
    D: TDSServerClass;
begin
  C := TDSServer.Create(nil);
  D := TDSServerClass.Create(nil);
  try
    C.Server := D;
    C.OnGetClass := OnGetClass;
    D.Start;
   
  finally
    D.Free;
    C.Free;
  end;
end;

procedure TForm1.OnGetClass(DSServerClass: TDSServerClass; var
    PersistentClass: TPersistentClass);
begin
  PersistentClass := TServerProvider;
end;

Use TDSProviderConnection to consume the in-process DataSnap service

We start hook up everything in DataSnap context to get it done:

var Q: TSQLConnection;
    D: TDSServer;
    C: TDSServerClass;
    P: TServerProvider;
    N: TDSProviderConnection;
begin
  P := TServerProvider.Create(nil);
  D := TDSServer.Create(nil);
  C := TDSServerClass.Create(nil);
  Q := TSQLConnection.Create(nil);
  N := TDSProviderConnection.Create(nil);
  try
    C.Server := D;
    C.OnGetClass := OnGetClass;

    D.Start;

    Q.DriverName := 'DSServer';
    Q.LoginPrompt := False;
    Q.Open;

    N.SQLConnection := Q;
    N.ServerClassName := 'TServerProvider';
    N.Connected := True;

    ClientDataSet1.RemoteServer := N;
    ClientDataSet1.ProviderName := 'DataSetProvider1';
    ClientDataSet1.Open;

    ShowMessage(IntToStr(ClientDataSet1.RecordCount));
  finally
    N.Free;
    Q.Free;
    C.Free;
    D.Free;
    P.Free;
  end;
end;

If you are using Delphi version 14.0.3513.24210 or prior than that, you will find it doesn’t work, a “Invalid pointer operation” exception raise after that.

I have found all the problems faced so far and the fixed are as follow.

Troubleshoot: Invalid pointer operation

There is a bug in DSUtil.StreamToDataPacket.  I have file a report in QC#78666.

Here is a fix without changing the DBX source code:

unit DSUtil.QC78666;

interface

implementation

uses SysUtils, Variants, VarUtils, ActiveX, Classes, DBXCommonResStrs, DSUtil,
     CodeRedirect;

type
  THeader = class
    const
      Empty       = 1;
      Variant     = 2;
      DataPacket  = 3;
  end;

  PIntArray = ^TIntArray;
  TIntArray = array[0..0] of Integer;

  TVarFlag = (vfByRef, vfVariant);
  TVarFlags = set of TVarFlag;

  EInterpreterError = class(Exception);

  TVariantStreamer = class
  private
    class function ReadArray(VType: Integer; const Data: TStream): OleVariant;
  public
    class function ReadVariant(out Flags: TVarFlags; const Data: TStream): OleVariant;
  end;

const
  EasyArrayTypes = [varSmallInt, varInteger, varSingle, varDouble, varCurrency,
                    varDate, varBoolean, varShortInt, varByte, varWord, varLongWord];

  VariantSize: array[0..varLongWord] of Word  = (0, 0, SizeOf(SmallInt), SizeOf(Integer),
    SizeOf(Single), SizeOf(Double), SizeOf(Currency), SizeOf(TDateTime), 0, 0,
    SizeOf(Integer), SizeOf(WordBool), 0, 0, 0, 0, SizeOf(ShortInt), SizeOf(Byte),
    SizeOf(Word), SizeOf(LongWord));

class function TVariantStreamer.ReadArray(VType: Integer; const Data: TStream): OleVariant;
var
  Flags: TVarFlags;
  LoDim, HiDim, Indices, Bounds: PIntArray;
  DimCount, VSize, i: Integer;
  V: OleVariant;
  LSafeArray: PSafeArray;
  P: Pointer;
begin
  VarClear(Result);
  Data.Read(DimCount, SizeOf(DimCount));
  VSize := DimCount * SizeOf(Integer);
  GetMem(LoDim, VSize);
  try
    GetMem(HiDim, VSize);
    try
      Data.Read(LoDim^, VSize);
      Data.Read(HiDim^, VSize);
      GetMem(Bounds, VSize * 2);
      try
        for i := 0 to DimCount - 1 do
        begin
          Bounds[i * 2] := LoDim[i];
          Bounds[i * 2 + 1] := HiDim[i];
        end;
        Result := VarArrayCreate(Slice(Bounds^,DimCount * 2), VType and varTypeMask);
      finally
        FreeMem(Bounds);
      end;
      if VType and varTypeMask in EasyArrayTypes then
      begin
        Data.Read(VSize, SizeOf(VSize));
        P := VarArrayLock(Result);
        try
          Data.Read(P^, VSize);
        finally
          VarArrayUnlock(Result);
        end;
      end else
      begin
        LSafeArray := PSafeArray(TVarData(Result).VArray);
        GetMem(Indices, VSize);
        try
          FillChar(Indices^, VSize, 0);
          for I := 0 to DimCount - 1 do
            Indices[I] := LoDim[I];
          while True do
          begin
            V := ReadVariant(Flags, Data);
            if VType and varTypeMask = varVariant then
              SafeArrayCheck(SafeArrayPutElement(LSafeArray, Indices^, V))
            else
              SafeArrayCheck(SafeArrayPutElement(LSafeArray, Indices^, TVarData(V).VPointer^));
            Inc(Indices[DimCount - 1]);
            if Indices[DimCount - 1] > HiDim[DimCount - 1] then
              for i := DimCount - 1 downto 0 do
                if Indices[i] > HiDim[i] then
                begin
                  if i = 0 then Exit;
                  Inc(Indices[i - 1]);
                  Indices[i] := LoDim[i];
                end;
          end;
        finally
          FreeMem(Indices);
        end;
      end;
    finally
      FreeMem(HiDim);
    end;
  finally
    FreeMem(LoDim);
  end;
end;

class function TVariantStreamer.ReadVariant(out Flags: TVarFlags; const Data: TStream): OleVariant;
var
  I, VType: Integer;
  W: WideString;
  TmpFlags: TVarFlags;
begin
  VarClear(Result);
  Flags := [];
  Data.Read(VType, SizeOf(VType));
  if VType and varByRef = varByRef then
    Include(Flags, vfByRef);
  if VType = varByRef then
  begin
    Include(Flags, vfVariant);
    Result := ReadVariant(TmpFlags, Data);
    Exit;
  end;
  if vfByRef in Flags then
    VType := VType xor varByRef;
  if (VType and varArray) = varArray then
    Result := ReadArray(VType, Data) else
  case VType and varTypeMask of
    varEmpty: VarClear(Result);
    varNull: Result := NULL;
    varOleStr:
    begin
      Data.Read(I, SizeOf(Integer));
      SetLength(W, I);
      Data.Read(W[1], I * 2);
      Result := W;
    end;
    varDispatch, varUnknown:
      raise EInterpreterError.CreateResFmt(@SBadVariantType,[IntToHex(VType,4)]);
  else
    TVarData(Result).VType := VType;
    Data.Read(TVarData(Result).VPointer, VariantSize[VType and varTypeMask]);
  end;
end;

procedure StreamToDataPacket(const Stream: TStream; out VarBytes: OleVariant);
var
  P: Pointer;
  ByteCount: Integer;
  Size: Int64;
begin
  Stream.Read(Size, 8);
  ByteCount := Integer(Size);
  if ByteCount > 0 then
  begin
    VarBytes := VarArrayCreate([0, ByteCount-1], varByte);
    P := VarArrayLock(VarBytes);
    try
//      Stream.Position := 0;   // QC#78666 "Mismatched in datapacket" with DSUtil.StreamToDataPacket
      Stream.Read(P^, ByteCount);
      Stream.Position := 0;
    finally
      VarArrayUnlock(VarBytes);
    end;
  end
  else
    VarBytes := Null;
end;

procedure StreamToVariantPatch(const Stream: TStream; out VariantValue: OleVariant);
var
  Flags: TVarFlags;
  Header: Byte;
begin
  if Assigned(Stream) then
  begin
    Stream.Position := 0;
    Stream.Read(Header, 1);
    if Header = THeader.Variant then
      VariantValue := TVariantStreamer.ReadVariant(Flags, Stream)
    else if Header = THeader.DataPacket then
      StreamToDataPacket(Stream, VariantValue)
    else
      Assert(false);
  end;
end;

var QC78666: TCodeRedirect;

initialization
  QC78666 := TCodeRedirect.Create(@StreamToVariant, @StreamToVariantPatch);
finalization
  QC78666.Free;
end.

Troubleshoot: I still encounter “Invalid Pointer Operation” after apply DSUtil.StreamToDataPacket patch

I have filed this problem in QC#78752.  An in-process DataSnap create an instance of TDSServerCommand.  A method of TDSServerCommand create TDBXNoOpRow instance:

function TDSServerCommand.CreateParameterRow: TDBXRow;
begin
  Result := TDBXNoOpRow.Create(FDbxContext);
end;

Most of the methods in TDBXNoOpRow is not implemented.  There are 2 methods in class TDBXNoOpRow, GetStream and SetStream are used in subsequence operations.  This is the reason that cause the exception.
After fix TDBXNoOpRow problem, the data packet will transport to ClientDataSet successfully.

The fix is as follow:

unit DBXCommonServer.QC78752;

interface

uses SysUtils, Classes, DBXCommon, DSCommonServer, DBXCommonTable;

type
  TDSServerCommand_Patch = class(TDSServerCommand)
  protected
    function CreateParameterRowPatch: TDBXRow;
  end;

  TDBXNoOpRowPatch = class(TDBXNoOpRow)
  private
    function GetBytesFromStreamReader(const R: TDBXStreamReader; out Buf: TBytes): Integer;
  protected
    procedure GetStream(DbxValue: TDBXStreamValue; var Stream: TStream; var IsNull:
        LongBool); override;
    procedure SetStream(DbxValue: TDBXStreamValue; StreamReader: TDBXStreamReader);
        override;
    function UseExtendedTypes: Boolean; override;
  end;

  TDBXStreamValueAccess = class(TDBXByteArrayValue)
  private
    FStreamStreamReader: TDBXLookAheadStreamReader;
  end;

implementation

uses CodeRedirect;

function TDSServerCommand_Patch.CreateParameterRowPatch: TDBXRow;
begin
  Result := TDBXNoOpRowPatch.Create(FDbxContext);
end;

procedure TDBXNoOpRowPatch.GetStream(DbxValue: TDBXStreamValue; var Stream: TStream;
    var IsNull: LongBool);
var iSize: integer;
    B: TBytes;
begin
  iSize := GetBytesFromStreamReader(TDBXStreamValueAccess(DbxValue).FStreamStreamReader, B);
  IsNull := iSize = 0;
  if not IsNull then begin
    Stream := TMemoryStream.Create;
    Stream.Write(B[0], iSize);
  end;
end;

procedure TDBXNoOpRowPatch.SetStream(DbxValue: TDBXStreamValue; StreamReader:
    TDBXStreamReader);
var B: TBytes;
    iSize: integer;
begin
  iSize := GetBytesFromStreamReader(StreamReader, B);
  Dbxvalue.SetDynamicBytes(0, B, 0, iSize);
end;

function TDBXNoOpRowPatch.GetBytesFromStreamReader(const R: TDBXStreamReader; out Buf: TBytes):
    Integer;
const BufSize = 50 * 1024;
var iPos: integer;
    iRead: integer;
begin
  Result := 0;
  while not R.Eos do begin
    SetLength(Buf, Result + BufSize);
    iPos := Result;
    iRead := R.Read(Buf, iPos, BufSize);
    Inc(Result, iRead);
  end;
  SetLength(Buf, Result);
end;

function TDBXNoOpRowPatch.UseExtendedTypes: Boolean;
begin
  Result := True;
end;

var QC78752: TCodeRedirect;

initialization
  QC78752 := TCodeRedirect.Create(@TDSServerCommand_Patch.CreateParameterRow, @TDSServerCommand_Patch.CreateParameterRowPatch);
finalization
  QC78752.Free;
end.

Troubleshoot: Both patches applied and work for the example but I still encounter “Invalid Pointer Operation”

This problem also filed in QC#78752.  The problem is due to the following 2 methods:

  1. procedure TDBXStreamValue.SetValue
  2. function TDBXLookAheadStreamReader.ConvertToMemoryStream: TStream;

TDBXLookAheadStreamReader.ConvertToMemoryStream return a managed FStream object to TDBXStreamValue.SetValue.  This stream object become another managed object of TDBXStreamValue.  It turns out that a Stream object managed by two objects and the exception raised when these 2 objects attempt to free the Stream object:

procedure TDBXStreamValue.SetValue(const Value: TDBXValue);
begin
  if Value.IsNull then
    SetNull
  else
  begin
    SetStream(Value.GetStream(False), True);
  end;
end;
function TDBXLookAheadStreamReader.ConvertToMemoryStream: TStream;
...
begin
  if FStream = nil then
    Result := nil
  else
  begin
    Count := Size;
    if not (FStream is TMemoryStream) then
    begin
      ...
      StreamTemp := FStream;
      FStream := Stream;
      FreeAndNil(StreamTemp);
    end;
    FStream.Seek(0, soFromBeginning);
    FHasLookAheadByte := false;
    Result := FStream;
  end;
end;

The fix is as follow:

unit DBXCommon.QC78752;

interface

implementation

uses SysUtils, Classes, DBXCommon, CodeRedirect;

type
  TDBXLookAheadStreamReaderAccess = class(TDBXStreamReader)
  private
    FStream: TStream;
    FEOS:               Boolean;
    FHasLookAheadByte:  Boolean;
    FLookAheadByte:     Byte;
  end;

  TDBXLookAheadStreamReaderHelper = class helper for TDBXLookAheadStreamReader
  private
    function Accessor: TDBXLookAheadStreamReaderAccess;
  public
    function ConvertToMemoryStreamPatch: TStream;
  end;

function TDBXLookAheadStreamReaderHelper.Accessor:
    TDBXLookAheadStreamReaderAccess;
begin
  Result := TDBXLookAheadStreamReaderAccess(Self);
end;

function TDBXLookAheadStreamReaderHelper.ConvertToMemoryStreamPatch: TStream;
var
  Stream: TMemoryStream;
  StreamTemp: TStream;
  Count: Integer;
  Buffer: TBytes;
  ReadBytes: Integer;
begin
  if Accessor.FStream = nil then
    Result := nil
  else
  begin
    Count := Size;
    if not (Accessor.FStream is TMemoryStream) then
    begin
      Stream := TMemoryStream.Create;
      if Count >= 0 then
        Stream.SetSize(Count);
      if Accessor.FHasLookAheadByte then
        Stream.Write(Accessor.FLookAheadByte, 1);
      SetLength(Buffer, 256);
      while true do
      begin
        ReadBytes := Accessor.FStream.Read(Buffer, Length(Buffer));
        if ReadBytes > 0 then
          Stream.Write(Buffer, ReadBytes)
        else
          Break;
      end;
      StreamTemp := Accessor.FStream;
      Accessor.FStream := Stream;
      FreeAndNil(StreamTemp);
      Result := Accessor.FStream;
    end else begin
      Stream := TMemoryStream.Create;
      Accessor.FStream.Seek(0, soFromBeginning);
      Stream.CopyFrom(Accessor.FStream, Accessor.FStream.Size);
    end;
    Stream.Seek(0, soFromBeginning);
    Accessor.FHasLookAheadByte := false;

    Result := Stream;
//    Stream := TMemoryStream.Create;
//    Stream.LoadFromStream(FStream);
//    FStream.Seek(0, soFromBeginning);
//    Result := Stream;
  end;
end;

var QC78752: TCodeRedirect;

initialization
  QC78752 := TCodeRedirect.Create(@TDBXLookAheadStreamReader.ConvertToMemoryStream, @TDBXLookAheadStreamReader.ConvertToMemoryStreamPatch);
finalization
  QC78752.Free;
end.

Troubleshoot: I encounter memory leaks after close the application

There is a memory leaks in TDSServerConnection for in-process connection.  I have filed a report in QC#78696.

Here is the fix:

unit DSServer.QC78696;

interface

implementation

uses SysUtils,
     DBXCommon, DSServer, DSCommonServer, DBXMessageHandlerCommon, DBXSqlScanner,
     DBXTransport,
     CodeRedirect;

type
  TDSServerConnectionHandlerAccess = class(TDBXConnectionHandler)
    FConProperties: TDBXProperties;
    FConHandle: Integer;
    FServer: TDSCustomServer;
    FDatabaseConnectionHandler: TObject;
    FHasServerConnection: Boolean;
    FInstanceProvider: TDSHashtableInstanceProvider;
    FCommandHandlers: TDBXCommandHandlerArray;
    FLastCommandHandler: Integer;
    FNextHandler: TDBXConnectionHandler;
    FErrorMessage: TDBXErrorMessage;
    FScanner: TDBXSqlScanner;
    FDbxConnection: TDBXConnection;
    FTransport: TDSServerTransport;
    FChannel: TDbxChannel;
    FCreateInstanceEventObject: TDSCreateInstanceEventObject;
    FDestroyInstanceEventObject: TDSDestroyInstanceEventObject;
    FPrepareEventObject: TDSPrepareEventObject;
    FConnectEventObject: TDSConnectEventObject;
    FErrorEventObject: TDSErrorEventObject;
    FServerCon: TDSServerConnection;
  end;

  TDSServerConnectionPatch = class(TDSServerConnection)
  public
    destructor Destroy; override;
  end;

  TDSServerDriverPatch = class(TDSServerDriver)
  protected
    function CreateConnectionPatch(ConnectionBuilder: TDBXConnectionBuilder): TDBXConnection;
  end;

destructor TDSServerConnectionPatch.Destroy;
begin
  inherited Destroy;
  TDSServerConnectionHandlerAccess(ServerConnectionHandler).FServerCon := nil;
  ServerConnectionHandler.Free;
end;

function TDSServerDriverPatch.CreateConnectionPatch(
  ConnectionBuilder: TDBXConnectionBuilder): TDBXConnection;
begin
  Result := TDSServerConnectionPatch.Create(ConnectionBuilder);
end;

var QC78696: TCodeRedirect;

initialization
  QC78696 := TCodeRedirect.Create(@TDSServerDriverPatch.CreateConnection, @TDSServerDriverPatch.CreateConnectionPatch);
finalization
  QC78696.Free;
end.