Skip to content

Commit 41a41d4

Browse files
danieleteticlaude
andcommitted
feat(ideexpert): Add Windows Service support to project wizard
- Add Windows Service templates (program_service.dpr.tpro, service.pas.tpro, service.dfm.tpro) - Use TDMVCFrameworkWindowsService class name instead of TMyWebModuleService - Fix ActiveRecord middleware support in Windows Service (removed incorrect code) - Add 7 Windows Service test cases to TestTemplateGenerator (27 tests total, all passing) - Add WizardTestStandalone VCL app for testing wizard without IDE - Clean up redundant build scripts (keep only build_and_run.bat and build_wizard_test.bat) - Fix service.dfm.tpro property order to resolve IDE errors - All 27 test combinations (console + Windows Service) compile successfully Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent e7b4e0b commit 41a41d4

20 files changed

+839
-55
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,3 +174,5 @@ nul
174174
/ideexpert/build_now.bat
175175
/ideexpert/build_wizard.bat
176176
/ideexpert/build_wizard2.bat
177+
/ideexpert/*.md
178+
/ideexpert/tests/*.md

ideexpert/DMVC.Expert.Commons.pas

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ interface
2929
uses
3030
MVCFramework.Commons,
3131
System.SysUtils,
32-
JsonDataObjects,
33-
ToolsAPI;
32+
JsonDataObjects;
3433

3534
type
3635
IGenCommand = interface
@@ -122,10 +121,9 @@ TProgramTypes = record
122121
FASTCGI_CONSOLE = 'fastcgi.console';
123122
APACHE = 'apache';
124123
ISAPI = 'isapi';
124+
WINDOWS_SERVICE = 'windows.service';
125125
end;
126126

127-
procedure ChangeIOTAModuleFileNamePrefix(const IOTA: IOTAModule; const FileNamePrefix: String);
128-
129127
implementation
130128

131129
uses
@@ -140,18 +138,4 @@ procedure TCustomCommand.CheckFor(const Key: String; Model: TJSONObject);
140138
end;
141139
end;
142140

143-
procedure ChangeIOTAModuleFileNamePrefix(const IOTA: IOTAModule; const FileNamePrefix: String);
144-
var
145-
lDirName: string;
146-
lFileName: string;
147-
lFileExt: string;
148-
begin
149-
lDirName := TPath.GetDirectoryName(IOTA.FileName);
150-
lFileName := TPath.GetFileNameWithoutExtension(IOTA.FileName);
151-
lFileExt := TPath.GetExtension(IOTA.FileName);
152-
lFileName := FileNamePrefix;
153-
// IOTA.FileName := TPath.Combine(lDirName, lFileName + lFileExt);
154-
// IOTA.Refresh(False);
155-
end;
156-
157141
end.

ideexpert/DMVC.Expert.Forms.NewProjectWizard.dfm

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -694,19 +694,32 @@ object frmDMVCNewProject: TfrmDMVCNewProject
694694
TabOrder = 10
695695
end
696696
end
697-
object rgServerType: TRadioGroup
697+
object rgServerProtocol: TRadioGroup
698698
Left = 24
699699
Top = 243
700-
Width = 297
700+
Width = 145
701701
Height = 92
702-
Caption = 'Server Application Type'
702+
Caption = 'Server Protocol'
703703
ItemIndex = 0
704704
Items.Strings = (
705-
'HTTP Console (Windows/Linux)'
706-
'HTTPS Console (Windows/Linux, requires TaurusTLS)'
707-
'FastCGI Console (Windows/Linux)')
705+
'HTTP'
706+
'HTTPS (requires TaurusTLS)'
707+
'FastCGI')
708708
TabOrder = 5
709709
end
710+
object rgApplicationType: TRadioGroup
711+
Left = 175
712+
Top = 243
713+
Width = 146
714+
Height = 92
715+
Caption = 'Application Type'
716+
ItemIndex = 0
717+
Items.Strings = (
718+
'Console (Win/Linux)'
719+
'Windows Service'
720+
'Linux Daemon')
721+
TabOrder = 11
722+
end
710723
object gbNameCase: TGroupBox
711724
Left = 340
712725
Top = 362

ideexpert/DMVC.Expert.Forms.NewProjectWizard.pas

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ TfrmDMVCNewProject = class(TForm)
9393
lblPATREON: TLabel;
9494
Image2: TImage;
9595
Shape2: TShape;
96-
rgServerType: TRadioGroup;
96+
rgServerProtocol: TRadioGroup;
97+
rgApplicationType: TRadioGroup;
9798
chkRateLimit: TCheckBox;
9899
chkJWT: TCheckBox;
99100
lblProjectName: TLabel;
@@ -196,7 +197,7 @@ procedure TfrmDMVCNewProject.ApplicationEventsIdle(Sender: TObject;
196197
begin
197198
chkProfileActions.Checked := False;
198199
end;
199-
case rgServerType.ItemIndex of
200+
case rgServerProtocol.ItemIndex of
200201
0: begin //http
201202
lblServerPort.Caption := 'HTTP Server Port';
202203
SyncServerPort('8080');
@@ -280,7 +281,7 @@ procedure TfrmDMVCNewProject.btnOKClick(Sender: TObject);
280281
begin
281282
lHints := lHints + ['- Include required FireDAC units in your project'];
282283
end;
283-
if rgServerType.ItemIndex = 1 then
284+
if rgServerProtocol.ItemIndex = 1 then
284285
begin
285286
lHints := lHints + ['- Install TaurusTLS from GetIT or directly from github (https://github.com/TurboPack/indy_extras)'];
286287
end;
@@ -316,8 +317,9 @@ procedure TfrmDMVCNewProject.FormCreate(Sender: TObject);
316317
UpdateProjectNameHint;
317318

318319
{$IF not Defined(FASTCGI)}
319-
rgServerType.Items.Delete(rgServerType.Items.Count-1);
320-
rgServerType.ItemIndex := 0;
320+
// FastCGI is only available from Delphi 13 Florence onwards
321+
rgServerProtocol.Items.Delete(rgServerProtocol.Items.Count-1);
322+
rgServerProtocol.ItemIndex := 0;
321323
{$ENDIF}
322324
end;
323325

@@ -504,12 +506,31 @@ function TfrmDMVCNewProject.GetConfigModel: TJSONObject;
504506
fModel.S[TConfigKey.websocket_unit_name] := 'WebSocketServerU';
505507
fModel.B[TConfigKey.websocket_generate] := chkWebSocketServer.Checked;
506508

507-
case rgServerType.ItemIndex of
508-
0: fModel.S[TConfigKey.program_type] := TProgramTypes.HTTP_CONSOLE;
509-
1: fModel.S[TConfigKey.program_type] := TProgramTypes.HTTPS_CONSOLE;
510-
2: fModel.S[TConfigKey.program_type] := TProgramTypes.FASTCGI_CONSOLE;
509+
// Determine program type based on Server Protocol + Application Type
510+
// Store server protocol for all types
511+
case rgServerProtocol.ItemIndex of
512+
0: fModel.S['program.server.protocol'] := 'http';
513+
1: fModel.S['program.server.protocol'] := 'https';
514+
2: fModel.S['program.server.protocol'] := 'fastcgi';
515+
end;
516+
517+
// Determine application type
518+
case rgApplicationType.ItemIndex of
519+
0: begin // Console
520+
case rgServerProtocol.ItemIndex of
521+
0: fModel.S[TConfigKey.program_type] := TProgramTypes.HTTP_CONSOLE;
522+
1: fModel.S[TConfigKey.program_type] := TProgramTypes.HTTPS_CONSOLE;
523+
2: fModel.S[TConfigKey.program_type] := TProgramTypes.FASTCGI_CONSOLE;
524+
end;
525+
end;
526+
1: begin // Windows Service
527+
fModel.S[TConfigKey.program_type] := TProgramTypes.WINDOWS_SERVICE;
528+
end;
529+
2: begin // Linux Daemon (future)
530+
fModel.S[TConfigKey.program_type] := 'linux.daemon';
531+
end;
511532
else
512-
raise Exception.Create('Invalid Server Type');
533+
raise Exception.Create('Invalid Application Type');
513534
end;
514535

515536
// Helper unit names (defaults, will be updated after units are created)

ideexpert/DMVC.Expert.ProjectGenerator.pas

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ class function TDMVCProjectGenerator.RenderTemplate(const ATemplateName: string;
229229
LTemplate.SetData('dmvc_version', GetDMVCVersion);
230230
LTemplate.SetData('webmodule_form_reference',
231231
'{' + AConfig.S[TConfigKey.webmodule_classname_short] + ': TWebModule}');
232+
// Form reference for Windows Service
233+
LTemplate.SetData('service_form_reference',
234+
'{DMVCFrameworkWindowsService: TDMVCFrameworkWindowsService}');
232235

233236
// Handler for missing variables - log warning but don't crash
234237
// For boolean checks ({{if var}}), undefined vars should evaluate to false
@@ -292,6 +295,20 @@ class procedure TDMVCProjectGenerator.Generate(const AProjectFolder, AProjectNam
292295
LogToFile('Project: ' + AProjectName);
293296
LogToFile('Folder: ' + AProjectFolder);
294297

298+
// Log key configuration values for debugging
299+
LogToFile('--- Configuration ---');
300+
LogToFile('program.type: ' + AConfig.S[TConfigKey.program_type]);
301+
if AConfig.Contains('program.server.protocol') then
302+
LogToFile('program.server.protocol: ' + AConfig.S['program.server.protocol']);
303+
LogToFile('program.default_server_port: ' + AConfig.S[TConfigKey.program_default_server_port]);
304+
LogToFile('controller.classname: ' + AConfig.S[TConfigKey.controller_classname]);
305+
LogToFile('webmodule.classname: ' + AConfig.S[TConfigKey.webmodule_classname]);
306+
if AConfig.Contains(TConfigKey.websocket_generate) then
307+
LogToFile('websocketserver.generate: ' + BoolToStr(AConfig.B[TConfigKey.websocket_generate], True));
308+
if AConfig.Contains(TConfigKey.jsonrpc_generate) then
309+
LogToFile('jsonrpc.generate: ' + BoolToStr(AConfig.B[TConfigKey.jsonrpc_generate], True));
310+
LogToFile('---------------------');
311+
295312
// Create project folder
296313
if not TDirectory.Exists(AProjectFolder) then
297314
TDirectory.CreateDirectory(AProjectFolder);
@@ -332,7 +349,18 @@ class procedure TDMVCProjectGenerator.Generate(const AProjectFolder, AProjectNam
332349
// Project files
333350
// Note: We only generate the .dpr file. Delphi will automatically create
334351
// the .dproj file when opening the project for the first time.
335-
SaveFile(AProjectName + '.dpr', RenderTemplate('program.dpr.tpro', AConfig));
352+
if AConfig.S[TConfigKey.program_type] = TProgramTypes.WINDOWS_SERVICE then
353+
begin
354+
// Windows Service uses different program template and adds ServiceU unit
355+
SaveFile(AProjectName + '.dpr', RenderTemplate('program_service.dpr.tpro', AConfig));
356+
SaveFile('ServiceU.pas', RenderTemplate('service.pas.tpro', AConfig));
357+
SaveFile('ServiceU.dfm', RenderTemplate('service.dfm.tpro', AConfig));
358+
end
359+
else
360+
begin
361+
// Console/FastCGI/Apache/ISAPI use standard program template
362+
SaveFile(AProjectName + '.dpr', RenderTemplate('program.dpr.tpro', AConfig));
363+
end;
336364

337365
// Required units
338366
SaveFile(CONTROLLER_UNIT + '.pas', RenderTemplate('controller.pas.tpro', AConfig));

ideexpert/DMVC.Expert.Templates.rc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
_LICENSE_HEADER RCDATA "templates\\_license_header.tpro"
1010
PROGRAM_DPR RCDATA "templates\\program.dpr.tpro"
11+
PROGRAM_SERVICE_DPR RCDATA "templates\\program_service.dpr.tpro"
12+
SERVICE_PAS RCDATA "templates\\service.pas.tpro"
13+
SERVICE_DFM RCDATA "templates\\service.dfm.tpro"
1114
CONTROLLER_PAS RCDATA "templates\\controller.pas.tpro"
1215
ENTITY_PAS RCDATA "templates\\entity.pas.tpro"
1316
WEBMODULE_PAS RCDATA "templates\\webmodule.pas.tpro"
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{{include "_license_header.tpro"}}
2+
program {{:program_name}};
3+
4+
uses
5+
Vcl.SvcMgr,
6+
System.SysUtils,
7+
{{if program_dotenv}}
8+
MVCFramework.DotEnv,
9+
{{endif}}
10+
ServiceU in 'ServiceU.pas' {{:service_form_reference}},
11+
{{:webmodule_unit_name}} in '{{:webmodule_unit_name}}.pas' {{:webmodule_form_reference}}{{if controller_unit_name}},
12+
{{:controller_unit_name}} in '{{:controller_unit_name}}.pas'{{endif}}{{if entity_generate}},
13+
{{:entity_unit_name}} in '{{:entity_unit_name}}.pas'{{endif}}{{if jsonrpc_generate}},
14+
{{:jsonrpc_unit_name}} in '{{:jsonrpc_unit_name}}.pas'{{endif}}{{if program_service_container_generate}},
15+
{{:program_service_container_unit_name}} in '{{:program_service_container_unit_name}}.pas'{{endif}}{{if program_ssv_mustache}},
16+
{{:mustache_helpers_unit_name}} in '{{:mustache_helpers_unit_name}}.pas'{{endif}}{{if program_ssv_templatepro}},
17+
{{:templatepro_helpers_unit_name}} in '{{:templatepro_helpers_unit_name}}.pas'{{endif}}{{if program_ssv_webstencils}},
18+
{{:webstencils_helpers_unit_name}} in '{{:webstencils_helpers_unit_name}}.pas'{{endif}}{{if websocketserver_generate}},
19+
{{:websocketserver_unit_name}} in '{{:websocketserver_unit_name}}.pas'{{endif}}{{if webmodule_middleware_activerecord}},
20+
MVCFramework.ActiveRecord{{endif}};
21+
22+
{$R *.RES}
23+
24+
begin
25+
// Windows 2003 Server requires StartServiceCtrlDispatcher to be
26+
// called before CoRegisterClassObject, which can be called indirectly
27+
// by Application.Initialize. TServiceApplication.DelayInitialize allows
28+
// Application.Initialize to be called from TService.Main (after
29+
// StartServiceCtrlDispatcher has been called).
30+
//
31+
// Delayed initialization of the Application object may affect
32+
// events which then occur prior to initialization, such as
33+
// TService.OnCreate. It is only recommended if the ServiceApplication
34+
// registers a class object with OLE and is intended for use with
35+
// Windows 2003 Server.
36+
//
37+
// Application.DelayInitialize := True;
38+
//
39+
if not Application.DelayInitialize or Application.Installing then
40+
Application.Initialize;
41+
Application.CreateForm(TDMVCFrameworkWindowsService, DMVCFrameworkWindowsService);
42+
Application.Run;
43+
end.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
object DMVCFrameworkWindowsService: TDMVCFrameworkWindowsService
2+
OnCreate = ServiceCreate
3+
AllowPause = False
4+
DisplayName = '{{:program_name}} RESTful Service'
5+
OnExecute = ServiceExecute
6+
OnStart = ServiceStart
7+
OnStop = ServiceStop
8+
Height = 150
9+
Width = 215
10+
end

0 commit comments

Comments
 (0)