Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(build, syncthing): dynamic console allocation for Windows#10335

Open
Shablone wants to merge 40 commits intosyncthing:mainfrom
Shablone:main
Open

feat(build, syncthing): dynamic console allocation for Windows#10335
Shablone wants to merge 40 commits intosyncthing:mainfrom
Shablone:main

Conversation

@Shablone
Copy link

@Shablone Shablone commented Sep 1, 2025

Dynamic Console Allocation for Windows

Purpose

Windows GUI builds either always show a short console pupup on windows systemstart implementation or never show one (CLI users can't see output).
See PR10334 , PR926 and many more

Solution

Implement dynamic console allocation:

  • No console when double-clicked from Explorer
  • Console appears when launched with CLI arguments
  • --no-console flag to explicitly disable console
  • Console behavior preserved across monitor restarts

Testing

syncthing.exe              # No console (GUI)
syncthing.exe --help       # Shows console + help
syncthing.exe --no-console # No console even with args

Opening from explorer won't show a console.

Implementation

  • Uses Windows API (AllocConsole/AttachConsole) to create console on demand
  • Monitor process passes --no-console to child processes when appropriate
  • Windows-only using build constraints

Fixes console window appearing briefly when launched from Explorer while maintaining proper CLI functionality.

Authorship

@Shablone Shablone changed the title Dynamic Console Allocation for Windows feat: Dynamic Console Allocation for Windows Sep 1, 2025
@github-actions github-actions bot added the enhancement New features or improvements of some kind, as opposed to a problem (bug) label Sep 1, 2025
@Shablone Shablone force-pushed the main branch 3 times, most recently from 78cad68 to c2552bf Compare September 1, 2025 21:57
@rafaeloledo
Copy link

Did you have success running it?

}

// Check if --no-console was passed or if we have no args (double-clicked)
noConsole := len(args) <= 1 // No args means launched from GUI
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noConsole := slices.Contains(args, "--no-console")

childArgs := args[1:] // Start with original args (excluding binary name)
if noConsole {
// Check if --no-console is already in the args
hasNoConsole := false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hasNoConsole := slices.Contains(childArgs, "--no-console")

procFreeConsole = kernel32.NewProc("FreeConsole")
procGetConsoleWindow = kernel32.NewProc("GetConsoleWindow")
procSetStdHandle = kernel32.NewProc("SetStdHandle")
procCreateFileW = kernel32.NewProc("CreateFileW")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateFile and SetStdHandle are in sys/x/windows

return nil // No command line arguments, don't allocate console
}

// Check if --no-console flag is present
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if slices.Contains(os.Args[1:], "--no-console") {
return nil
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Prefer if they actually used the arguments parser we have...)

@rasa
Copy link
Member

rasa commented Sep 2, 2025

One issue with this is if a user runs this GUI version from inside an SSH session, they won't see any output. The console window will appear on the desktop.

Copy link
Member

@calmh calmh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the approach that we didn't get to work previously. If it does work, i.e. behaves reasonably in a normal terminal etc, that's cool. I don't see why you're reinventing command line parsing multiple times instead of using the parsed command line though?

@tomasz1986 tomasz1986 changed the title feat: Dynamic Console Allocation for Windows feat(build, syncthing): dynamic console allocation for Windows Sep 2, 2025
@Shablone
Copy link
Author

Shablone commented Sep 2, 2025

Thank you for the review :)
I will rework that changes accordingly.

I was reinventing the stuff because

  • It was late
  • I'm not familiar with Go
  • I didn't get familiar with the remaining codebase
    Sorry for that

I will also investigate the ssh session. Although it seems very niche to me, to ssh into a windows server.

@calmh
Copy link
Member

calmh commented Sep 2, 2025

I think the SSH bit is indeed extremely niche, but probably follows the same scenario as just running the command in an existing cmd? At least the latter should work, imho. (by which I mean "show output in that terminal")

@calmh
Copy link
Member

calmh commented Sep 2, 2025

This seems somewhat promising. I'll have comments on the actual code and stuff at some point, but just some behavioural notes;

  • When doubleclicking syncthing.exe from explorer, there is a console flash but it disappears. That's good, but even better if it were possible to avoid the flash? If not, no biggie, it's already at the same level as the old --no-console was while it worked.

  • This is possibly an extension for a future PR but, currently, if Syncthing is already running and is then doubleclicked again, visually nothing happens (except the flash as above). I think it would be nice and make sense if it resulted in the equivalent of synchthing.exe browser, i.e. start the UI. (Perhaps that's not on you and should just be the default behavior when launching Syncthing a second time under any platform and circumstance. Something to think about.)

@Shablone
Copy link
Author

Shablone commented Sep 2, 2025

I noticed that syncthing not only was starting in console mode, it also opened the browser at runtime via calling cmd.exe resulting in another flash if no console was present yet.
I included the fix here
I haven't checked, but I guess this was the reason for your mentioned scenario @calmh

@Shablone Shablone force-pushed the main branch 2 times, most recently from 8357fc0 to 79e5eb0 Compare September 3, 2025 17:48
@Shablone Shablone requested review from calmh and rasa September 3, 2025 19:34
@Shablone
Copy link
Author

Hey
I'm not sure if I just need to be patient, but is there anything missing for a merge?

@tomasz1986
Copy link
Member

I'm trying to test the PR right now, but for me, even when launched from Explorer, the console window just stays open.

This is under Windows 10 x64, and the default shell is set to Command Prompt.

Copy link
Member

@calmh calmh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some structural notes

}

// User explicitly disabled console -> don't allocate console
if slices.Contains(os.Args[1:], "--no-console") {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see no reason to do this instead of using our command line parser? (If there is one it's fine, but requires explanation. If it's about having a console available for when --help gets called, I suspect there is a way around that using the binding callback on the --no-console option perhaps...)

return nil
}

// SSH sessions -> don't allocate console
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not, what happens otherwise?

// Try to attach to parent consol
if ret, _, err := procAttachConsole.Call(uintptr(ATTACH_PARENT_PROCESS)); ret != 0 {
return redirectStdHandles()
} else if err != syscall.Errno(0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be !errors.Is(err, syscall.Errno(0)) and errors.As below instead of the direct case

if ret, _, err := procAllocConsole.Call(); ret != 0 {
consoleAllocated = true
return redirectStdHandles()
} else if err != syscall.Errno(0) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(As above)


// FreeConsole releases the console
func FreeConsole() {
if consoleAllocated {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather see this as a return value from InitConsole, to avoid keeping the global around...

@Shablone Shablone marked this pull request as draft September 16, 2025 16:22
@Shablone Shablone marked this pull request as ready for review September 28, 2025 20:44
@Shablone
Copy link
Author

Alright. Please have another look

I now splitted the console initialization into attachment (Already on present) and allocation (a new one needed).
Relying on Kong would otherwise be very complex. The console handle should already be there if --help was passed or some invalid arguments. Unfortunately both don't reach Kongs AfterApply(). I also can't use the other hooks of kong, as they wouldn't have parsed the arguments yet.
But I like the new approach better then the manual flag check. So now I always try to to attach to a console if present. Then we can parse the arguments and only create a new console if needed.

I also tested the ssh stuff. With the current commit when I ssh from unix into windows and call synthing.exe, it will pipe the output to my ssh shell. No new console appears 👍

I also learned that FreeConsole() is not needed, as windows this stuff on its own on process termination. So I could delete the defering stuff

I removed the old HideConsole and replaced it with the dynamic allowcation

@Shablone Shablone requested a review from calmh September 28, 2025 20:57
@Shablone
Copy link
Author

Shablone commented Sep 28, 2025

Please keep in mind, that for testing there should be no newer version online, which does not have this feature implemented. Or auto-upgrade should be disabled.
Otherwise it will upgrade itself and run as console again

@syncthing syncthing deleted a comment from calmh Sep 29, 2025
Comment on lines +21 to +23
// 2. cmd.exe /C start (fallback): Traditional approach that spawns a command
// shell. Less secure (potential command injection) but works on all Windows
// versions including very old ones.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure we don't run on those Windows versions...

Copy link
Member

@calmh calmh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, sorry for taking some time on this, I do want to get it in. Can you sort out the various style/linter complaints please, and then I think this is ready given it passes some manual testing 🙏

Comment on lines +220 to +223
var consoleAttached = false
if err := osutil.AttachConsole(); err == nil {
consoleAttached = true
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var consoleAttached = false
if err := osutil.AttachConsole(); err == nil {
consoleAttached = true
}
consoleAttached := osutil.AttachConsole() == nil

if we don't care about the error, I guess

@Shablone
Copy link
Author

Thanks, sorry for taking some time on this, I do want to get it in. Can you sort out the various style/linter complaints please, and then I think this is ready given it passes some manual testing 🙏

Thank you. There is no hurry 🙂

@calmh
Copy link
Member

calmh commented Oct 23, 2025

Actual Windows users who want this, can we have some testing? 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New features or improvements of some kind, as opposed to a problem (bug)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants