/[AnywhereTS-MSSQL]/trunk/TSAdminTool/frmConfigServices.cs
ViewVC logotype

Contents of /trunk/TSAdminTool/frmConfigServices.cs

Parent Directory Parent Directory | Revision Log Revision Log


Revision 64 - (show annotations) (download)
Thu Jul 12 18:56:31 2012 UTC (8 years ago) by william
File size: 28332 byte(s)
+ more exception logging

1 using System;
2 using System.Collections.Generic;
3 using System.ComponentModel;
4 using System.Data;
5 using System.Drawing;
6 using System.Text;
7 using System.Windows.Forms;
8 using System.IO;
9 using System.Security.AccessControl;
10 using System.Security.Principal;
11
12 namespace AnywhereTS
13 {
14 public partial class frmConfigServices : Form
15 {
16 public frmConfigServices()
17 {
18 InitializeComponent();
19 }
20
21 private atsDataSet.TftpServerDataTable datatableTFTP; // TFTP server directories
22 private atsDataSet.TerminalServerDataTable datatableTerminalServer; // terminal servers
23 const string strATSshareName = @"AnywhereTS$"; // The name of the AnywhereTS Win32 file share for the local TFTP server.
24 const string strShareDesc = @"Used by AnywhereTS";
25 private List<string> tftpUncPaths; // List with the corresponding UNC paths for items in lstTFTP.
26
27 private void frmConfigureMode_Load(object sender, EventArgs e)
28 {
29 //this.Cursor = Cursors.WaitCursor;
30
31 tftpUncPaths = new List<string>(); // Create a list for the TFTP UNC paths.
32
33 // Setup database
34 if (ProSupport.tftpServerTableAdapter == null || ProSupport.terminalServerTableAdapter == null) // Is database initiated?
35 {
36 // Databases is not initiated, do it now
37 ProSupport.InitDatabase();
38 }
39
40 datatableTFTP = new atsDataSet.TftpServerDataTable();
41 ProSupport.tftpServerTableAdapter.Fill(datatableTFTP);
42 datatableTerminalServer = new atsDataSet.TerminalServerDataTable();
43 ProSupport.terminalServerTableAdapter.Fill(datatableTerminalServer);
44
45 if (ATSGlobals.configured == 0)
46 { // ATS not configured yet
47 RadTSthisComputer.Checked = true;
48 txtTFTPpath.Text = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + @"\AnywhereTS\TFTP";
49 radTFTPinternal.Checked = true;
50 }
51 else
52 { // ATS already configured.
53 txtTFTPpath.Text = ATSGlobals.strTFTPdir;
54 radTFTPinternal.Checked = (ATSGlobals.tftpConfig == 0);
55 radTFTPexternal.Checked = (ATSGlobals.tftpConfig == 1);
56 if (ATSGlobals.terminalServerConfig == 0)
57 { // This computer is terminal server
58
59 if (datatableTerminalServer.Rows.Count == 0) //Any other terminal servers?
60 { // No other terminal servers
61 RadTSthisComputer.Checked = true;
62 }
63 else
64 { // At least one other
65 radTSboth.Checked = true;
66 }
67 }
68 else
69 { // This computer is not terminal server
70 radTSother.Checked = true;
71 }
72
73 }
74
75
76 // Update terminal server list
77 UpdateTerminalServerList();
78
79 // Update TFTP server directories list
80 if (ATSGlobals.tftpConfig == 1)
81 {
82 UpdateTftpList();
83 }
84
85 helpProvider.HelpNamespace = ATSGlobals.strHelpFilePath; // Initiate helpProvider
86
87 UpdateControls();
88
89 this.Cursor = Cursors.Default;
90 }
91
92
93 private void btnOK_Click(object sender, EventArgs e)
94 {
95 this.Cursor = Cursors.WaitCursor;
96
97 if (RadTSthisComputer.Checked)
98 ATSGlobals.terminalServerConfig = 0;
99 else if (radTSboth.Checked)
100 ATSGlobals.terminalServerConfig = 0;
101 else if (radTSother.Checked)
102 ATSGlobals.terminalServerConfig = 1;
103 else
104 {
105 string error = "Error: Undefined terminal servers config (42001)";
106 using (log4net.NDC.Push(string.Format("terminalServerConfig={0}", ATSGlobals.terminalServerConfig)))
107 {
108 Logging.ATSAdminLog.Error(error);
109 }
110 MessageBox.Show(string.Format("{0} -> {1}", error, string.Format("terminalServerConfig={0}", ATSGlobals.terminalServerConfig)));
111 return;
112 }
113
114
115 string strPath; // The TFTP path
116 strPath = txtTFTPpath.Text.Trim();
117
118 if (radTFTPinternal.Checked)
119 { // We are using the internal TFTP
120 ATSGlobals.tftpConfig = 0; // Use ATS TFTP
121 // Validate TFTP directory
122 if (strPath.Length == 0)
123 {
124 this.Cursor = Cursors.Default;
125 MessageBox.Show("You must specifiy a directory for the AnywhereTS shared network files.");
126 txtTFTPpath.Focus();
127 return;
128 }
129
130 if (strPath.StartsWith(".") || strPath.StartsWith(@"\"))
131 {
132 this.Cursor = Cursors.Default;
133 MessageBox.Show("Invalid path for the AnywhereTS shared network files.");
134 using (log4net.NDC.Push(string.Format("path={0}", strPath)))
135 {
136 Logging.ATSAdminLog.Error("Invalid path for the AnywhereTS shared network files.");
137 }
138 txtTFTPpath.Focus();
139 return;
140 }
141
142 // Create TFTP directory
143 try
144 {
145 Directory.CreateDirectory(strPath);
146 }
147 catch (Exception ex)
148 {
149 this.Cursor = Cursors.Default;
150 MessageBox.Show("Could not create directory '" + strPath + "'. Error: " + ex.Message);
151 using (log4net.NDC.Push(string.Format("SqlException: MESSAGE={0}{1}Diagnostics:{1}{2}", ex.Message, System.Environment.NewLine, ex.ToString())))
152 {
153 using (log4net.NDC.Push(string.Format("directory={0}", strPath)))
154 {
155 Logging.ATSAdminLog.Error("Could not create directory.");
156 }
157 }
158 txtTFTPpath.Select();
159 return;
160 }
161
162 // Check that the directory is local
163 ShareUtil shUtil = new ShareUtil(); // Create a util class
164 if (shUtil.IsNetworkPath(strPath))
165 { // The path is a network path
166 MessageBox.Show("A network path cannot be used for the local TFTP server. Please enter a local path.");
167 txtTFTPpath.Select();
168 return;
169 }
170 // Save the TFTP directory to registry
171 ATSGlobals.strTFTPdir = strPath; // Set the C# version of TFTP dir
172 Globals.strTFTPdir = strPath; // Set the VB version of TFTP dir
173 ATSGlobals.SetATSRegValue(ATSGlobals.strRegTFTP_root, ATSGlobals.strTFTPdir);
174
175 // Delete all TFTP roots from database
176 foreach (DataRow row in datatableTFTP.Rows)
177 {
178 row.Delete();
179 }
180 ProSupport.tftpServerTableAdapter.Update(datatableTFTP);
181
182 if (!RadTSthisComputer.Checked)
183 { // There are external terminal servers.
184
185 // Create a share, save the UNC path to the database, add rights to TFTP directory
186 AddTFTPpath(ATSGlobals.strTFTPdir);
187 }
188 else
189 { // We are only using this computer as terminal server
190 SetTFTPrights(ATSGlobals.strTFTPdir);
191 }
192 }
193 else
194 {
195 // Use other TFTP
196 ATSGlobals.tftpConfig = 1; // Use other TFTP
197
198 if (lstTFTP.Items.Count == 0)
199 {
200 this.Cursor = Cursors.Default;
201 MessageBox.Show("You must choose at least one TFTP server root directory, where AnywhereTS can place client images and configurations.");
202 btnAddTFTPpath.Focus();
203 return;
204 }
205 }
206
207 // Check if we are using external terminal servers
208 if (RadTSthisComputer.Checked)
209 { // Only this computer is a terminal server. Delete all other terminal servers.
210 foreach (DataRow row in datatableTerminalServer.Rows)
211 {
212 row.Delete();
213 }
214 ProSupport.terminalServerTableAdapter.Update(datatableTerminalServer);
215 }
216 else
217 { // We have external terminal servers
218 if (lstTerminalServer.Items.Count == 0)
219 {
220 this.Cursor = Cursors.Default;
221 MessageBox.Show("You must specify at least one Terminal Server!");
222 btnAddTerminalServer.Focus();
223 return;
224 }
225
226 // Close database connections
227 datatableTFTP.Dispose();
228 datatableTerminalServer.Dispose();
229 ProSupport.clientTableAdapter.Connection.Close();
230 ProSupport.terminalServerTableAdapter.Connection.Close();
231 ProSupport.clientTableAdapter.Dispose();
232 ProSupport.terminalServerTableAdapter.Dispose();
233
234 // Configure SQL browser service (needed for the control panel to be able to access the database).
235
236 bool sqlServerRestarted = DatabaseSupport.EnableNamedPipes(); // Enable named pipes in the SQL server
237 DatabaseSupport.AutostartSQLbrowserService(); // Configure the SQL Browser service to autostart.
238 DatabaseSupport.StartSQLbrowserService(); // Start the SQL Browser service
239
240 if (sqlServerRestarted)
241 {
242 // SQL server was restarted. Try to reconnect to database in order to repair the database connectivity
243 ProSupport.InitDatabase();
244
245 try
246 {
247 datatableTFTP = new atsDataSet.TftpServerDataTable();
248 ProSupport.tftpServerTableAdapter.Fill(datatableTFTP); // This operation causes an error and forces the connectivity to recover.
249 }
250 catch
251 { // Do nothing, just catch the error that will most likely occur here.
252 }
253 finally
254 {
255 System.Threading.Thread.Sleep(5000); // Wait for the SQL connection to recover.
256 }
257 }
258 }
259
260 // Save DHCP options
261 if (radAtsDhcp.Checked)
262 {
263 ATSGlobals.dhcpConfig = 0; // Use the internal DCHP server
264 }
265 else
266 {
267 ATSGlobals.dhcpConfig = 1; // Use the other DCHP server
268 }
269
270 // Save TFTP configuration to registry
271 ATSGlobals.SetATSRegValue(ATSGlobals.strRegTFTPconfig,ATSGlobals.tftpConfig);
272
273 // Save DCHP configuration to registry
274 ATSGlobals.SetATSRegValue(ATSGlobals.strRegDHCPconfig, ATSGlobals.dhcpConfig);
275
276 // Terminate window
277 this.Cursor = Cursors.Default;
278 DialogResult = DialogResult.OK;
279 }
280
281
282 private void AddTFTPpath(string strPath)
283 {
284 string uncPath; // The path in UNC format
285 string strLocalPath = ""; // Local path to save in database for display purposes. Only set if path is local
286
287 ShareUtil shUtil = new ShareUtil(); // Create a util class
288
289 // Check if path is a network path
290 if (shUtil.IsNetworkPath(strPath))
291 { // The path is a network path
292 if (strPath.StartsWith("\\"))
293 { // The network path is a UNC path, no need to convert it
294 uncPath = strPath;
295 }
296 else
297 { // The network path is not a UNC path, convert it
298 uncPath = UNCconverter.ConvertLocalPathToUnc(strPath);
299 }
300 }
301 else
302 { // User is adding a local path
303 strLocalPath = strPath;
304 string strThisComputer = Environment.MachineName; // The name of this computer
305 // Compile the UNC path
306 uncPath = @"\\" + strThisComputer + @"\" + strATSshareName;
307
308 // Delete ATS share if it exists
309 shUtil.DeleteShare(strATSshareName);
310
311 // Delete share from database if it exists
312 ProSupport.tftpServerTableAdapter.Fill(datatableTFTP); // Refresh data table
313 DataRow rowToDelete;
314 rowToDelete = datatableTFTP.Rows.Find(uncPath);
315 if (rowToDelete != null)
316 { // The local TFTP path exists in database, delete it
317 rowToDelete.Delete();
318 ProSupport.tftpServerTableAdapter.Update(datatableTFTP);
319 }
320
321 // Create network share
322
323 uint nRetVal;
324 nRetVal = shUtil.CreateShare(strShareDesc, strATSshareName, strPath);
325 if (nRetVal == 0)
326 {
327 // Share created
328 }
329 else
330 {
331 // Error
332 MessageBox.Show("Could not create a share for the AnywereTS TFTP data. Do you have sufficient rights? (25322)");
333 return;
334 }
335
336 }
337
338 // Add the remote or local directory path to the database
339 if (uncPath.Length <= 255)
340 { // The path will fit in the database
341 atsDataSet.TftpServerRow row = (atsDataSet.TftpServerRow)datatableTFTP.NewRow();
342 row["Path"] = uncPath;
343 row["LocalPath"] = strLocalPath;
344
345 try
346 {
347 datatableTFTP.AddTftpServerRow(row);
348 }
349 catch (ConstraintException)
350 {
351 MessageBox.Show("'" + uncPath + "' is already in the list.");
352 return;
353 }
354
355 ProSupport.tftpServerTableAdapter.Update(datatableTFTP);
356
357 // Ask if we are going to add righs to the TFTP root directory, set rights.
358 SetTFTPrights(row["Path"].ToString());
359 UpdateTftpList(); // Update the list box
360 lstTFTP.Select();
361 }
362 else
363 { // Path is too long
364 MessageBox.Show("The directory path is too long, please use a path that is max 255 character");
365 }
366
367 }
368
369 private void radAtsDhcp_CheckedChanged(object sender, EventArgs e)
370 {
371 UpdateControls(); // Set text "Next >" or "Finish" on the Ok button
372 }
373
374 private void radExternalDhcp_CheckedChanged(object sender, EventArgs e)
375 {
376 UpdateControls(); // Set text "Next >" or "Finish" on the Ok button
377 }
378
379 private void UpdateControls()
380 {
381 if (RadTSthisComputer.Checked)
382 { // Only this computer as terminal server
383 lblTerminalServers.Enabled = false;
384 lstTerminalServer.Enabled = false;
385 btnAddTerminalServer.Enabled = false;
386 btnDeleteTerminalServer.Enabled = false;
387 }
388 else
389 { // Other terminal servers
390 lblTerminalServers.Enabled = true;
391 lstTerminalServer.Enabled = true;
392 btnAddTerminalServer.Enabled = true;
393 if (lstTerminalServer.SelectedIndex == -1)
394 btnDeleteTerminalServer.Enabled = false;
395 else
396 btnDeleteTerminalServer.Enabled = true;
397 }
398
399 if (radTFTPinternal.Checked)
400 {
401 lblTFTPpath.Enabled = true;
402 txtTFTPpath.Enabled = true;
403 btnBrowseTFTP.Enabled = true;
404 lblTFTPdirectories.Enabled = false;
405 lstTFTP.Enabled = false;
406 btnAddTFTPpath.Enabled = false;
407 btnDeleteTFTP.Enabled = false;
408 radAtsDhcp.Enabled = true;
409 lstTFTP.SelectedIndex = -1;
410 btnOK.Text = "Next >";
411 }
412 else
413 {
414 lblTFTPdirectories.Enabled = true;
415 lstTFTP.Enabled = true;
416 btnAddTFTPpath.Enabled = true;
417 if (lstTFTP.SelectedIndex == -1)
418 btnDeleteTFTP.Enabled = false;
419 else
420 btnDeleteTFTP.Enabled = true;
421 lblTFTPpath.Enabled = false;
422 txtTFTPpath.Enabled = false;
423 btnBrowseTFTP.Enabled = false;
424 btnOK.Text = "Finish";
425 radAtsDhcp.Enabled = false;
426 radExternalDhcp.Checked = true;
427 }
428 }
429
430 private void radTFTPinternal_CheckedChanged(object sender, EventArgs e)
431 {
432 UpdateControls();
433 }
434
435 private void radTFTPexternal_CheckedChanged(object sender, EventArgs e)
436 {
437 if (radTFTPexternal.Checked && ATSGlobals.tftpConfig == 0)
438 { // We have just de-selected internal TFTP server
439
440 // Delete all TFTP roots from database
441 foreach (DataRow row in datatableTFTP.Rows)
442 {
443 row.Delete();
444 }
445 ProSupport.tftpServerTableAdapter.Update(datatableTFTP);
446 UpdateTftpList();
447
448 // Delete the file share for the internal TFTP directory if it exists.
449 ShareUtil shUtil = new ShareUtil(); // Create a util class
450 shUtil.DeleteShare("AnywhereTS$");
451
452 // ATS is now in a non configured state
453 ATSGlobals.configured = 0;
454 ATSGlobals.SetATSRegValue(ATSGlobals.strRegConfigured, ATSGlobals.configured);
455 }
456 UpdateControls();
457 }
458
459 // Update the list with TFTP directories from database.
460 private void UpdateTftpList()
461 {
462 lstTFTP.Items.Clear();
463 tftpUncPaths.Clear();
464 ProSupport.tftpServerTableAdapter.Fill(datatableTFTP);
465 foreach (DataRow row in datatableTFTP.Rows)
466 { // Display local path if possible
467
468
469 lstTFTP.Items.Add(ProSupport.GetTftpPath(row));
470 //string gg = row["Path"].ToString();
471 //tftpUncPaths.Add(gg);
472 tftpUncPaths.Add(row["Path"].ToString()); // Add the UNC path to the list.
473 }
474 btnDeleteTFTP.Enabled = false;
475 }
476
477 // Update the list with terminal servers from database.
478 private void UpdateTerminalServerList()
479 {
480 lstTerminalServer.Items.Clear();
481 foreach (DataRow row in datatableTerminalServer.Rows)
482 {
483 lstTerminalServer.Items.Add(row["Path"].ToString());
484 }
485 btnDeleteTerminalServer.Enabled = false;
486 }
487
488 private void lstTFTP_SelectedIndexChanged(object sender, EventArgs e)
489 {
490 if (lstTFTP.SelectedIndex == -1)
491 { // No item selcted in the TFTP directory list
492 btnDeleteTFTP.Enabled = false;
493 }
494 else
495 { // User has selcted an item in the list
496 btnDeleteTFTP.Enabled = true;
497 }
498 }
499
500 // Delete the row selected in the TFTP directory list box.
501 private void btnDeleteTFTP_Click(object sender, EventArgs e)
502 {
503 DataRow rowToDelete;
504 rowToDelete = datatableTFTP.Rows.Find(tftpUncPaths[lstTFTP.SelectedIndex]);
505 rowToDelete.Delete();
506 ProSupport.tftpServerTableAdapter.Update(datatableTFTP);
507 UpdateTftpList(); // Update the list box
508
509 if (datatableTFTP.Rows.Count == 0)
510 { // Last row deleted
511 // ATS is now in a non configured state
512 ATSGlobals.configured = 0;
513 ATSGlobals.SetATSRegValue(ATSGlobals.strRegConfigured, ATSGlobals.configured);
514 }
515 }
516
517 // Set user rights to TFTP directory, all remote desktop users should be able to write files.
518 void SetTFTPrights(string path)
519 {
520 // Ask if we are going to add righs to the TFTP root directory
521 DialogResult resultRights;
522 resultRights = MessageBox.Show(this, "In order for users to use the AnywhereTS control panel, they need rights to write in the TFTP root directory. Do you want AnywhereTS to add these rights now?", "AnywhereTS", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question, MessageBoxDefaultButton.Button1);
523 if (resultRights == DialogResult.Yes)
524 {
525 try
526 {
527 // Create a new DirectoryInfo object.
528 DirectoryInfo dInfo = new DirectoryInfo(path);
529
530 // Get a DirectorySecurity object that represents the
531 // current security settings.
532 DirectorySecurity dSecurity = dInfo.GetAccessControl();
533
534 // Add a FileSystemAccessRule to the security settings, to allow write.
535 // No general rights to delete files. These rights are set on the individual files.
536 dSecurity.AddAccessRule(new FileSystemAccessRule(new SecurityIdentifier(WellKnownSidType.BuiltinRemoteDesktopUsersSid, null), FileSystemRights.WriteData, AccessControlType.Allow));
537
538 // Set the new access settings.
539 dInfo.SetAccessControl(dSecurity);
540
541 }
542 catch (Exception ex)
543 {
544 this.Cursor = Cursors.Default;
545 MessageBox.Show("Could not set user access rights for TFTP directory. Do you have sufficient right? (29991). Error: " + ex.Message);
546 using (log4net.NDC.Push(string.Format("SqlException: MESSAGE={0}{1}Diagnostics:{1}{2}", ex.Message, System.Environment.NewLine, ex.ToString())))
547 {
548 Logging.ATSAdminLog.Error("Could not set user access rights for TFTP directory.");
549 }
550 return;
551 }
552 }
553 }
554
555 private void btnDeleteTerminalServer_Click(object sender, EventArgs e)
556 {
557 DataRow rowToDelete;
558 rowToDelete = datatableTerminalServer.Rows.Find(lstTerminalServer.SelectedItem);
559 rowToDelete.Delete();
560 ProSupport.terminalServerTableAdapter.Update(datatableTerminalServer);
561 UpdateTerminalServerList(); // Update the list box
562 }
563
564 private void btnAddTerminalServer_Click(object sender, EventArgs e)
565 {
566 // Show dialog
567 frmAddTerminalServer addTSdialog = new frmAddTerminalServer();
568 DialogResult result = addTSdialog.ShowDialog();
569 if (result == DialogResult.OK)
570 {
571 // Add the server to the database
572 if (addTSdialog.inputstring.Trim().Length <= 255)
573 { // The path will fit in the database
574 atsDataSet.TerminalServerRow row = (atsDataSet.TerminalServerRow)datatableTerminalServer.NewRow();
575 row["Path"] = addTSdialog.inputstring.Trim();
576 try
577 {
578 datatableTerminalServer.AddTerminalServerRow(row);
579 }
580 catch (ConstraintException)
581 {
582 MessageBox.Show("'" + row["Path"] + "' is already in the list.");
583 return;
584 }
585
586 ProSupport.terminalServerTableAdapter.Update(datatableTerminalServer);
587
588 UpdateTerminalServerList(); // Update the list box
589 lstTerminalServer.Select();
590 }
591 else
592 { // Path is too long
593 MessageBox.Show("The server path is too long, please use a path that is max 255 character");
594 }
595 }
596 }
597
598 private void lstTerminalServer_SelectedIndexChanged(object sender, EventArgs e)
599 {
600 if (lstTerminalServer.SelectedIndex == -1)
601 { // No item selcted in the TFTP directory list
602 btnDeleteTerminalServer.Enabled = false;
603 }
604 else
605 { // User has selcted an item in the list
606 btnDeleteTerminalServer.Enabled = true;
607 }
608 }
609
610 private void RadTSthisComputer_CheckedChanged(object sender, EventArgs e)
611 {
612 UpdateControls();
613 }
614
615 private void radTSother_CheckedChanged(object sender, EventArgs e)
616 {
617 UpdateControls();
618 }
619
620 private void radTSboth_CheckedChanged(object sender, EventArgs e)
621 {
622 UpdateControls();
623 }
624
625 // Add a TFTP server
626 private void btnAddTFTPpath_Click(object sender, EventArgs e)
627 {
628 // Show dialog
629 frmAddTFTP addTFTPdialog = new frmAddTFTP();
630 DialogResult result = addTFTPdialog.ShowDialog();
631 if (result == DialogResult.OK)
632 {
633 string strPath;
634 strPath = addTFTPdialog.inputstring.Trim();
635
636 if (Directory.Exists(strPath))
637 {
638 // Convert to UNC, share local directory, add directory to database
639 AddTFTPpath(strPath);
640 }
641 else
642 {
643 MessageBox.Show("Error: Cannot find or access the directory. Please check name and rights. (34212)");
644 }
645 UpdateTftpList();
646 }
647 }
648
649 private void btnBrowseTFTP_Click(object sender, EventArgs e)
650 {
651 // Show the FolderBrowserDialog.
652 Rebrowse:
653 DialogResult result = folderBrowserDialog.ShowDialog();
654 if (result == DialogResult.OK)
655 {
656 string strPath=""; // The path provided by the user
657
658 // Browse to a folder by displaying the folder browse Dialog
659 try
660 {
661 strPath = folderBrowserDialog.SelectedPath.Trim();
662 }
663 catch (NotSupportedException)
664 { // User has browsed to a non folder
665 MessageBox.Show("Object is not a file folder.");
666 using (log4net.NDC.Push(string.Format("path={0}", strPath)))
667 {
668 Logging.ATSAdminLog.Error("Object is not a file folder.");
669 }
670 goto Rebrowse;
671 }
672 txtTFTPpath.Text = strPath;
673 txtTFTPpath.Select();
674 }
675 }
676 }
677 }
678

  ViewVC Help
Powered by ViewVC 1.1.22