@xevy/heny-connect 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,7 @@
1
1
  param(
2
- [Parameter(Mandatory = $true)]
3
- [string] $NodePath,
4
-
5
- [Parameter(Mandatory = $true)]
6
- [string] $CliPath,
7
-
2
+ [Parameter(Mandatory = $true)][string] $NodePath,
3
+ [Parameter(Mandatory = $true)][string] $CliPath,
4
+ [Parameter(Mandatory = $true)][string] $HomePath,
8
5
  [string] $IconPath,
9
6
  [switch] $Install,
10
7
  [switch] $Uninstall
@@ -13,21 +10,23 @@ param(
13
10
  $ErrorActionPreference = "Stop"
14
11
  $taskName = "HenyConnect"
15
12
  $powershellPath = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
13
+ $statusPath = Join-Path $HomePath "worker-status.json"
14
+ $trayStatusPath = Join-Path $HomePath "tray-status.json"
15
+ $pausePath = Join-Path $HomePath "paused"
16
+ $stopPath = Join-Path $HomePath "stopping"
16
17
  if (-not $IconPath) { $IconPath = Join-Path $PSScriptRoot "heny.ico" }
17
18
 
18
19
  function Get-TaskArguments {
19
- return "-NoLogo -NoProfile -STA -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$PSCommandPath`" -NodePath `"$NodePath`" -CliPath `"$CliPath`" -IconPath `"$IconPath`""
20
+ return "-NoLogo -NoProfile -STA -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$PSCommandPath`" -NodePath `"$NodePath`" -CliPath `"$CliPath`" -HomePath `"$HomePath`" -IconPath `"$IconPath`""
20
21
  }
21
22
 
22
23
  function Install-HenyTask {
23
24
  param([bool] $StartNow)
24
-
25
25
  $existing = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
26
26
  if ($existing) {
27
27
  Stop-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
28
28
  Unregister-ScheduledTask -TaskName $taskName -Confirm:$false
29
29
  }
30
-
31
30
  $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent().Name
32
31
  $action = New-ScheduledTaskAction -Execute $powershellPath -Argument (Get-TaskArguments)
33
32
  $trigger = New-ScheduledTaskTrigger -AtLogOn -User $currentUser
@@ -55,10 +54,7 @@ if ($Install) {
55
54
 
56
55
  $createdNew = $false
57
56
  $mutex = [Threading.Mutex]::new($true, "Local\HenyConnectTray", [ref] $createdNew)
58
- if (-not $createdNew) {
59
- $mutex.Dispose()
60
- exit 0
61
- }
57
+ if (-not $createdNew) { $mutex.Dispose(); exit 0 }
62
58
 
63
59
  Add-Type -AssemblyName System.Windows.Forms
64
60
  Add-Type -AssemblyName System.Drawing
@@ -69,212 +65,161 @@ $iconPaused = [Drawing.Icon]::new((Join-Path $PSScriptRoot "heny-paused.ico"))
69
65
  $iconError = [Drawing.Icon]::new((Join-Path $PSScriptRoot "heny-error.ico"))
70
66
  $notifyIcon = [Windows.Forms.NotifyIcon]::new()
71
67
  $notifyIcon.Icon = $iconPaused
72
- $notifyIcon.Text = "Heny Connect - Connecting"
68
+ $notifyIcon.Text = "Heny Connect - Browser starting"
73
69
  $notifyIcon.Visible = $true
74
70
 
75
71
  $menu = [Windows.Forms.ContextMenuStrip]::new()
76
72
  $titleItem = [Windows.Forms.ToolStripMenuItem]::new("Heny Connect")
77
73
  $titleItem.Enabled = $false
78
74
  $titleItem.Font = [Drawing.Font]::new($titleItem.Font, [Drawing.FontStyle]::Bold)
79
- $statusItem = [Windows.Forms.ToolStripMenuItem]::new("Status: Connecting...")
75
+ $statusItem = [Windows.Forms.ToolStripMenuItem]::new("Status: Browser starting")
80
76
  $statusItem.Enabled = $false
77
+ $actionItem = [Windows.Forms.ToolStripMenuItem]::new("Current action: None")
78
+ $actionItem.Enabled = $false
81
79
  $openItem = [Windows.Forms.ToolStripMenuItem]::new("Open Heny")
82
80
  $pauseItem = [Windows.Forms.ToolStripMenuItem]::new("Pause")
83
81
  $reconnectItem = [Windows.Forms.ToolStripMenuItem]::new("Reconnect now")
84
82
  $startItem = [Windows.Forms.ToolStripMenuItem]::new("Start at sign-in")
85
- $startItem.CheckOnClick = $false
86
83
  $startItem.Checked = [bool] (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue)
87
84
  $quitItem = [Windows.Forms.ToolStripMenuItem]::new("Quit")
88
-
89
- $null = $menu.Items.Add($titleItem)
90
- $null = $menu.Items.Add($statusItem)
91
- $null = $menu.Items.Add([Windows.Forms.ToolStripSeparator]::new())
92
- $null = $menu.Items.Add($openItem)
93
- $null = $menu.Items.Add($pauseItem)
94
- $null = $menu.Items.Add($reconnectItem)
95
- $null = $menu.Items.Add($startItem)
96
- $null = $menu.Items.Add([Windows.Forms.ToolStripSeparator]::new())
97
- $null = $menu.Items.Add($quitItem)
85
+ foreach ($item in @($titleItem, $statusItem, $actionItem, [Windows.Forms.ToolStripSeparator]::new(), $openItem, $pauseItem, $reconnectItem, $startItem, [Windows.Forms.ToolStripSeparator]::new(), $quitItem)) { $null = $menu.Items.Add($item) }
98
86
  $notifyIcon.ContextMenuStrip = $menu
99
87
 
100
- $script:paused = $false
101
- $script:heartbeatProcess = $null
102
- $script:heartbeatState = $null
103
- $script:nextHeartbeat = Get-Date
104
- $script:lastStatus = "connecting"
88
+ $script:workerProcess = $null
89
+ $script:nextStart = Get-Date
90
+ $script:paused = Test-Path -LiteralPath $pausePath
105
91
  $script:exiting = $false
106
92
 
107
93
  function Set-HenyStatus {
108
- param(
109
- [ValidateSet("available", "paused", "connecting", "error")]
110
- [string] $State,
111
- [string] $Detail
112
- )
113
-
94
+ param([string] $State, [string] $Action)
114
95
  $label = switch ($State) {
115
96
  "available" { "Available" }
97
+ "working" { "Working" }
116
98
  "paused" { "Paused" }
117
- "connecting" { "Connecting..." }
118
- "error" { "Connection error" }
99
+ "pausing" { "Pausing" }
100
+ "browser_starting" { "Browser starting" }
101
+ "error" { "Error" }
102
+ "offline" { "Offline" }
103
+ default { "Connecting" }
119
104
  }
120
105
  $statusItem.Text = "Status: $label"
121
- if ($Detail) { $statusItem.ToolTipText = $Detail }
106
+ $actionItem.Text = if ($Action) { "Current action: $Action" } else { "Current action: None" }
122
107
  $notifyIcon.Text = "Heny Connect - $label"
123
- $notifyIcon.Icon = switch ($State) {
124
- "available" { $iconAvailable }
125
- "error" { $iconError }
126
- default { $iconPaused }
127
- }
128
- $script:lastStatus = $State
108
+ $notifyIcon.Icon = if ($State -in @("available", "working")) { $iconAvailable } elseif ($State -eq "error") { $iconError } else { $iconPaused }
109
+ }
110
+
111
+ function Write-TrayStatus {
112
+ param([string] $State, [int] $ExitCode = 0, [string] $Detail = "")
113
+ @{ state = $State; exitCode = $ExitCode; detail = $Detail.Substring(0, [Math]::Min(200, $Detail.Length)); updatedAt = [DateTime]::UtcNow.ToString("o") } | ConvertTo-Json -Compress | Set-Content -LiteralPath $trayStatusPath -Encoding UTF8
129
114
  }
130
115
 
131
116
  function Get-HenyUrl {
132
117
  try {
133
- $homePath = if ($env:HENY_CONNECT_HOME) { $env:HENY_CONNECT_HOME } else { [Environment]::GetFolderPath("UserProfile") }
134
- $statePath = Join-Path $homePath ".heny-connect.json"
135
- $state = Get-Content -LiteralPath $statePath -Raw | ConvertFrom-Json
118
+ $state = Get-Content -LiteralPath (Join-Path $HomePath ".heny-connect.json") -Raw | ConvertFrom-Json
136
119
  return ([string] $state.server).TrimEnd("/") + "/desktop"
137
- } catch {
138
- return "https://heny.vyte.dev/desktop"
139
- }
120
+ } catch { return "https://heny.vyte.dev/desktop" }
140
121
  }
141
122
 
142
- function Stop-HeartbeatProcess {
143
- if (-not $script:heartbeatProcess) { return }
123
+ function Stop-Worker {
124
+ if (-not $script:workerProcess) { return }
144
125
  try {
145
- if (-not $script:heartbeatProcess.HasExited) {
146
- $script:heartbeatProcess.Kill()
147
- $script:heartbeatProcess.WaitForExit(2000) | Out-Null
126
+ if (-not $script:workerProcess.HasExited) {
127
+ New-Item -ItemType Directory -Path $HomePath -Force | Out-Null
128
+ Set-Content -LiteralPath $pausePath -Value "paused" -NoNewline
129
+ Set-Content -LiteralPath $stopPath -Value "stop" -NoNewline
130
+ $limit = (Get-Date).AddSeconds(5)
131
+ while (-not $script:workerProcess.HasExited -and (Get-Date) -lt $limit) {
132
+ try {
133
+ $state = Get-Content -LiteralPath $statusPath -Raw | ConvertFrom-Json
134
+ if ($state.state -eq "paused") { break }
135
+ } catch {}
136
+ Start-Sleep -Milliseconds 200
137
+ }
138
+ if (-not $script:workerProcess.HasExited) {
139
+ & taskkill.exe /PID ([string] $script:workerProcess.Id) /T /F 2>$null | Out-Null
140
+ $script:workerProcess.WaitForExit(2000) | Out-Null
141
+ }
148
142
  }
149
- $script:heartbeatProcess.Dispose()
143
+ $script:workerProcess.Dispose()
150
144
  } catch {}
151
- $script:heartbeatProcess = $null
152
- $script:heartbeatState = $null
145
+ $script:workerProcess = $null
153
146
  }
154
147
 
155
- function Start-Heartbeat {
156
- param(
157
- [ValidateSet("available", "offline")]
158
- [string] $State = "available"
159
- )
160
-
161
- if ($script:heartbeatProcess) { return }
162
- $detail = if ($State -eq "offline") { "$env:COMPUTERNAME`: paused in tray" } else { "$env:COMPUTERNAME`: browser ready" }
148
+ function Start-Worker {
149
+ if ($script:workerProcess -or $script:paused -or $script:exiting) { return }
150
+ Remove-Item -LiteralPath $stopPath -Force -ErrorAction SilentlyContinue
163
151
  $startInfo = [Diagnostics.ProcessStartInfo]::new()
164
152
  $startInfo.FileName = $NodePath
165
- $startInfo.Arguments = "`"$CliPath`" heartbeat --state $State --detail `"$detail`""
153
+ $startInfo.Arguments = "`"$CliPath`" run"
166
154
  $startInfo.UseShellExecute = $false
167
155
  $startInfo.CreateNoWindow = $true
168
156
  $startInfo.WindowStyle = [Diagnostics.ProcessWindowStyle]::Hidden
169
- $startInfo.RedirectStandardOutput = $true
170
- $startInfo.RedirectStandardError = $true
157
+ $startInfo.EnvironmentVariables["HENY_CONNECT_HOME"] = $HomePath
171
158
  $process = [Diagnostics.Process]::new()
172
159
  $process.StartInfo = $startInfo
173
160
  try {
174
- if (-not $process.Start()) { throw "The heartbeat process did not start." }
175
- $script:heartbeatProcess = $process
176
- $script:heartbeatState = $State
177
- if ($State -eq "available") { Set-HenyStatus -State "connecting" -Detail "Contacting Heny" }
161
+ if (-not $process.Start()) { throw "The worker did not start." }
162
+ $script:workerProcess = $process
163
+ Write-TrayStatus -State "worker_started"
164
+ Set-HenyStatus -State "browser_starting"
178
165
  } catch {
179
166
  $process.Dispose()
180
- Set-HenyStatus -State "error" -Detail $_.Exception.Message
181
- $script:nextHeartbeat = (Get-Date).AddSeconds(30)
167
+ Write-TrayStatus -State "worker_start_failed" -ExitCode -1 -Detail $_.Exception.Message
168
+ Set-HenyStatus -State "error"
169
+ $script:nextStart = (Get-Date).AddSeconds(15)
182
170
  }
183
171
  }
184
172
 
185
- function Complete-Heartbeat {
186
- if (-not $script:heartbeatProcess -or -not $script:heartbeatProcess.HasExited) { return }
187
- $process = $script:heartbeatProcess
188
- $state = $script:heartbeatState
189
- $stdout = $process.StandardOutput.ReadToEnd().Trim()
190
- $stderr = $process.StandardError.ReadToEnd().Trim()
191
- $exitCode = $process.ExitCode
192
- $process.Dispose()
193
- $script:heartbeatProcess = $null
194
- $script:heartbeatState = $null
195
- $script:nextHeartbeat = (Get-Date).AddSeconds(30)
196
-
197
- if ($exitCode -eq 0) {
198
- if ($state -eq "offline" -or $script:paused) {
199
- Set-HenyStatus -State "paused" -Detail "Paused on this computer"
200
- } else {
201
- Set-HenyStatus -State "available" -Detail "Connected to Heny"
202
- }
203
- return
204
- }
205
-
206
- $detail = if ($stderr) { $stderr } elseif ($stdout) { $stdout } else { "Heartbeat exited with code $exitCode" }
207
- $previousStatus = $script:lastStatus
208
- Set-HenyStatus -State "error" -Detail $detail
209
- if ($previousStatus -ne "error") {
210
- $notifyIcon.ShowBalloonTip(4000, "Heny Connect", $detail, [Windows.Forms.ToolTipIcon]::Warning)
211
- }
173
+ function Request-Pause {
174
+ New-Item -ItemType Directory -Path $HomePath -Force | Out-Null
175
+ Set-Content -LiteralPath $pausePath -Value "paused" -NoNewline
176
+ $script:paused = $true
177
+ $pauseItem.Text = "Resume"
178
+ Set-HenyStatus -State "pausing"
212
179
  }
213
180
 
214
- function Send-OfflineAndWait {
215
- Stop-HeartbeatProcess
216
- $startInfo = [Diagnostics.ProcessStartInfo]::new()
217
- $startInfo.FileName = $NodePath
218
- $startInfo.Arguments = "`"$CliPath`" heartbeat --state offline --detail `"$env:COMPUTERNAME`: tray closed`""
219
- $startInfo.UseShellExecute = $false
220
- $startInfo.CreateNoWindow = $true
221
- $startInfo.WindowStyle = [Diagnostics.ProcessWindowStyle]::Hidden
222
- $process = [Diagnostics.Process]::Start($startInfo)
223
- if ($process) {
224
- $process.WaitForExit(10000) | Out-Null
225
- if (-not $process.HasExited) { $process.Kill() }
226
- $process.Dispose()
227
- }
181
+ function Request-Resume {
182
+ Remove-Item -LiteralPath $pausePath -Force -ErrorAction SilentlyContinue
183
+ Remove-Item -LiteralPath $stopPath -Force -ErrorAction SilentlyContinue
184
+ $script:paused = $false
185
+ $pauseItem.Text = "Pause"
186
+ $script:nextStart = Get-Date
187
+ Set-HenyStatus -State "browser_starting"
228
188
  }
229
189
 
230
- $openHeny = {
231
- Start-Process (Get-HenyUrl)
232
- }
190
+ $openHeny = { Start-Process (Get-HenyUrl) }
233
191
  $openItem.Add_Click($openHeny)
234
192
  $notifyIcon.Add_DoubleClick($openHeny)
235
-
236
- $pauseItem.Add_Click({
237
- if ($script:paused) {
238
- $script:paused = $false
239
- $pauseItem.Text = "Pause"
240
- $script:nextHeartbeat = Get-Date
241
- Set-HenyStatus -State "connecting" -Detail "Resuming"
242
- } else {
243
- $script:paused = $true
244
- $pauseItem.Text = "Resume"
245
- Stop-HeartbeatProcess
246
- Start-Heartbeat -State "offline"
247
- }
248
- })
249
-
250
- $reconnectItem.Add_Click({
251
- $script:paused = $false
252
- $pauseItem.Text = "Pause"
253
- Stop-HeartbeatProcess
254
- $script:nextHeartbeat = Get-Date
255
- Set-HenyStatus -State "connecting" -Detail "Reconnecting"
256
- })
193
+ $pauseItem.Add_Click({ if ($script:paused) { Request-Resume } else { Request-Pause } })
194
+ $reconnectItem.Add_Click({ Request-Pause; Stop-Worker; Request-Resume })
257
195
 
258
196
  $startItem.Add_Click({
259
197
  try {
260
198
  if ($startItem.Checked) {
261
199
  Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
262
200
  $startItem.Checked = $false
263
- $notifyIcon.ShowBalloonTip(2500, "Heny Connect", "Start at sign-in is off.", [Windows.Forms.ToolTipIcon]::Info)
264
201
  } else {
265
202
  Install-HenyTask -StartNow $false
266
203
  $startItem.Checked = $true
267
- $notifyIcon.ShowBalloonTip(2500, "Heny Connect", "Start at sign-in is on.", [Windows.Forms.ToolTipIcon]::Info)
268
204
  }
269
- } catch {
270
- Set-HenyStatus -State "error" -Detail $_.Exception.Message
271
- }
205
+ } catch { Set-HenyStatus -State "error" }
272
206
  })
273
207
 
274
208
  $quitItem.Add_Click({
275
209
  $script:exiting = $true
276
210
  $timer.Stop()
277
- Send-OfflineAndWait
211
+ Request-Pause
212
+ $limit = (Get-Date).AddSeconds(5)
213
+ while ((Get-Date) -lt $limit) {
214
+ try {
215
+ $state = Get-Content -LiteralPath $statusPath -Raw | ConvertFrom-Json
216
+ if ($state.state -eq "paused") { break }
217
+ } catch {}
218
+ Start-Sleep -Milliseconds 200
219
+ }
220
+ Stop-Worker
221
+ Remove-Item -LiteralPath $pausePath -Force -ErrorAction SilentlyContinue
222
+ Remove-Item -LiteralPath $stopPath -Force -ErrorAction SilentlyContinue
278
223
  $notifyIcon.Visible = $false
279
224
  [Windows.Forms.Application]::ExitThread()
280
225
  })
@@ -282,19 +227,32 @@ $quitItem.Add_Click({
282
227
  $timer = [Windows.Forms.Timer]::new()
283
228
  $timer.Interval = 1000
284
229
  $timer.Add_Tick({
285
- Complete-Heartbeat
286
- if (-not $script:paused -and -not $script:heartbeatProcess -and (Get-Date) -ge $script:nextHeartbeat) {
287
- Start-Heartbeat -State "available"
230
+ if ($script:workerProcess -and $script:workerProcess.HasExited) {
231
+ $code = $script:workerProcess.ExitCode
232
+ Write-TrayStatus -State "worker_exited" -ExitCode $code
233
+ $script:workerProcess.Dispose()
234
+ $script:workerProcess = $null
235
+ if (-not $script:paused) {
236
+ Set-HenyStatus -State "error"
237
+ $script:nextStart = if ($code -in @(2, 3)) { [DateTime]::MaxValue } else { (Get-Date).AddSeconds(15) }
238
+ }
288
239
  }
240
+ if ($script:workerProcess) {
241
+ try {
242
+ $state = Get-Content -LiteralPath $statusPath -Raw | ConvertFrom-Json
243
+ if ($script:paused -and $state.state -ne "paused") { Set-HenyStatus -State "pausing" } else { Set-HenyStatus -State ([string] $state.state) -Action ([string] $state.currentAction) }
244
+ } catch {}
245
+ } elseif ($script:paused) { Set-HenyStatus -State "paused" }
246
+ if (-not $script:paused -and -not $script:workerProcess -and (Get-Date) -ge $script:nextStart) { Start-Worker }
289
247
  })
290
248
  $timer.Start()
291
- Start-Heartbeat -State "available"
249
+ if ($script:paused) { Set-HenyStatus -State "paused"; $pauseItem.Text = "Resume" } else { Start-Worker }
292
250
 
293
251
  try {
294
252
  [Windows.Forms.Application]::Run()
295
253
  } finally {
296
254
  $timer.Stop()
297
- Stop-HeartbeatProcess
255
+ Stop-Worker
298
256
  $notifyIcon.Visible = $false
299
257
  $notifyIcon.Dispose()
300
258
  $menu.Dispose()