@fastybird/smart-panel-backend 1.1.0-alpha.41 → 1.1.0-alpha.42

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.
Files changed (101) hide show
  1. package/dist/app.module.js +1 -1
  2. package/dist/app.module.js.map +1 -1
  3. package/dist/common/utils/cancellable-exec.utils.d.ts +5 -0
  4. package/dist/common/utils/cancellable-exec.utils.js +34 -0
  5. package/dist/common/utils/cancellable-exec.utils.js.map +1 -0
  6. package/dist/common/utils/remote-access-observation.utils.d.ts +11 -0
  7. package/dist/common/utils/remote-access-observation.utils.js +89 -0
  8. package/dist/common/utils/remote-access-observation.utils.js.map +1 -0
  9. package/dist/modules/remote-access/controllers/remote-access.controller.js +9 -5
  10. package/dist/modules/remote-access/controllers/remote-access.controller.js.map +1 -1
  11. package/dist/modules/remote-access/models/provider.model.d.ts +3 -0
  12. package/dist/modules/remote-access/models/provider.model.js +23 -0
  13. package/dist/modules/remote-access/models/provider.model.js.map +1 -1
  14. package/dist/modules/remote-access/models/status.model.d.ts +2 -0
  15. package/dist/modules/remote-access/models/status.model.js +18 -1
  16. package/dist/modules/remote-access/models/status.model.js.map +1 -1
  17. package/dist/modules/remote-access/models/urls.model.d.ts +2 -0
  18. package/dist/modules/remote-access/models/urls.model.js +17 -0
  19. package/dist/modules/remote-access/models/urls.model.js.map +1 -1
  20. package/dist/modules/remote-access/platforms/remote-access-provider.platform.d.ts +18 -1
  21. package/dist/modules/remote-access/remote-access.constants.d.ts +2 -1
  22. package/dist/modules/remote-access/remote-access.constants.js +2 -1
  23. package/dist/modules/remote-access/remote-access.constants.js.map +1 -1
  24. package/dist/modules/remote-access/services/remote-access-proxy-contribution.service.d.ts +1 -0
  25. package/dist/modules/remote-access/services/remote-access-proxy-contribution.service.js +4 -1
  26. package/dist/modules/remote-access/services/remote-access-proxy-contribution.service.js.map +1 -1
  27. package/dist/modules/remote-access/services/remote-access-status.service.d.ts +40 -8
  28. package/dist/modules/remote-access/services/remote-access-status.service.js +314 -65
  29. package/dist/modules/remote-access/services/remote-access-status.service.js.map +1 -1
  30. package/dist/modules/remote-access/services/remote-access-url.service.d.ts +2 -2
  31. package/dist/modules/remote-access/services/remote-access-url.service.js +4 -2
  32. package/dist/modules/remote-access/services/remote-access-url.service.js.map +1 -1
  33. package/dist/modules/websocket/gateway/websocket.gateway.js +1 -0
  34. package/dist/modules/websocket/gateway/websocket.gateway.js.map +1 -1
  35. package/dist/plugins/remote-access-cloudflare-tunnel/controllers/setup.controller.d.ts +5 -3
  36. package/dist/plugins/remote-access-cloudflare-tunnel/controllers/setup.controller.js +20 -8
  37. package/dist/plugins/remote-access-cloudflare-tunnel/controllers/setup.controller.js.map +1 -1
  38. package/dist/plugins/remote-access-cloudflare-tunnel/controllers/status.controller.d.ts +3 -5
  39. package/dist/plugins/remote-access-cloudflare-tunnel/controllers/status.controller.js +11 -10
  40. package/dist/plugins/remote-access-cloudflare-tunnel/controllers/status.controller.js.map +1 -1
  41. package/dist/plugins/remote-access-cloudflare-tunnel/models/status.model.d.ts +2 -0
  42. package/dist/plugins/remote-access-cloudflare-tunnel/models/status.model.js +17 -0
  43. package/dist/plugins/remote-access-cloudflare-tunnel/models/status.model.js.map +1 -1
  44. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-managed.service.d.ts +20 -2
  45. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-managed.service.js +105 -48
  46. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-managed.service.js.map +1 -1
  47. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-provider.service.d.ts +13 -1
  48. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-provider.service.js +8 -2
  49. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflare-tunnel-provider.service.js.map +1 -1
  50. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-cli.service.d.ts +1 -1
  51. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-cli.service.js +12 -24
  52. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-cli.service.js.map +1 -1
  53. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-metrics.service.d.ts +1 -1
  54. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-metrics.service.js +5 -2
  55. package/dist/plugins/remote-access-cloudflare-tunnel/services/cloudflared-metrics.service.js.map +1 -1
  56. package/dist/plugins/remote-access-tailscale/controllers/setup.controller.d.ts +10 -5
  57. package/dist/plugins/remote-access-tailscale/controllers/setup.controller.js +105 -15
  58. package/dist/plugins/remote-access-tailscale/controllers/setup.controller.js.map +1 -1
  59. package/dist/plugins/remote-access-tailscale/controllers/status.controller.d.ts +5 -4
  60. package/dist/plugins/remote-access-tailscale/controllers/status.controller.js +19 -9
  61. package/dist/plugins/remote-access-tailscale/controllers/status.controller.js.map +1 -1
  62. package/dist/plugins/remote-access-tailscale/models/status.model.d.ts +16 -0
  63. package/dist/plugins/remote-access-tailscale/models/status.model.js +100 -1
  64. package/dist/plugins/remote-access-tailscale/models/status.model.js.map +1 -1
  65. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.constants.d.ts +2 -0
  66. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.constants.js +3 -1
  67. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.constants.js.map +1 -1
  68. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.exceptions.d.ts +17 -1
  69. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.exceptions.js +33 -1
  70. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.exceptions.js.map +1 -1
  71. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.openapi.d.ts +2 -2
  72. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.openapi.js +1 -0
  73. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.openapi.js.map +1 -1
  74. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.plugin.js +2 -0
  75. package/dist/plugins/remote-access-tailscale/remote-access-tailscale.plugin.js.map +1 -1
  76. package/dist/plugins/remote-access-tailscale/services/tailscale-cli.service.d.ts +8 -4
  77. package/dist/plugins/remote-access-tailscale/services/tailscale-cli.service.js +58 -14
  78. package/dist/plugins/remote-access-tailscale/services/tailscale-cli.service.js.map +1 -1
  79. package/dist/plugins/remote-access-tailscale/services/tailscale-login.service.d.ts +8 -3
  80. package/dist/plugins/remote-access-tailscale/services/tailscale-login.service.js +175 -151
  81. package/dist/plugins/remote-access-tailscale/services/tailscale-login.service.js.map +1 -1
  82. package/dist/plugins/remote-access-tailscale/services/tailscale-node-managed.service.d.ts +51 -7
  83. package/dist/plugins/remote-access-tailscale/services/tailscale-node-managed.service.js +481 -101
  84. package/dist/plugins/remote-access-tailscale/services/tailscale-node-managed.service.js.map +1 -1
  85. package/dist/plugins/remote-access-tailscale/services/tailscale-operation-coordinator.service.d.ts +33 -0
  86. package/dist/plugins/remote-access-tailscale/services/tailscale-operation-coordinator.service.js +167 -0
  87. package/dist/plugins/remote-access-tailscale/services/tailscale-operation-coordinator.service.js.map +1 -0
  88. package/dist/plugins/remote-access-tailscale/services/tailscale-provider.service.d.ts +13 -1
  89. package/dist/plugins/remote-access-tailscale/services/tailscale-provider.service.js +8 -2
  90. package/dist/plugins/remote-access-tailscale/services/tailscale-provider.service.js.map +1 -1
  91. package/dist/plugins/remote-access-tailscale/services/tailscale-serve.service.d.ts +1 -1
  92. package/dist/plugins/remote-access-tailscale/services/tailscale-serve.service.js +7 -6
  93. package/dist/plugins/remote-access-tailscale/services/tailscale-serve.service.js.map +1 -1
  94. package/dist/plugins/remote-access-tailscale/services/tailscale-setup.service.d.ts +1 -3
  95. package/dist/plugins/remote-access-tailscale/services/tailscale-setup.service.js +9 -11
  96. package/dist/plugins/remote-access-tailscale/services/tailscale-setup.service.js.map +1 -1
  97. package/dist/plugins/remote-access-tailscale/utils/tailscale-control.utils.d.ts +5 -0
  98. package/dist/plugins/remote-access-tailscale/utils/tailscale-control.utils.js +36 -0
  99. package/dist/plugins/remote-access-tailscale/utils/tailscale-control.utils.js.map +1 -0
  100. package/dist/tsconfig.build.tsbuildinfo +1 -1
  101. package/package.json +2 -2
@@ -8,20 +8,24 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
8
8
  var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
11
14
  var __importDefault = (this && this.__importDefault) || function (mod) {
12
15
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
16
  };
14
17
  Object.defineProperty(exports, "__esModule", { value: true });
15
18
  exports.TailscaleNodeManagedService = void 0;
16
19
  exports.compareTailscaleVersions = compareTailscaleVersions;
17
- const node_child_process_1 = require("node:child_process");
18
20
  const os_1 = __importDefault(require("os"));
19
21
  const path_1 = require("path");
20
22
  const common_1 = require("@nestjs/common");
21
23
  const config_1 = require("@nestjs/config");
22
24
  const event_emitter_1 = require("@nestjs/event-emitter");
23
25
  const logger_1 = require("../../../common/logger");
26
+ const cancellable_exec_utils_1 = require("../../../common/utils/cancellable-exec.utils");
24
27
  const config_utils_1 = require("../../../common/utils/config.utils");
28
+ const remote_access_observation_utils_1 = require("../../../common/utils/remote-access-observation.utils");
25
29
  const config_service_1 = require("../../../modules/config/services/config.service");
26
30
  const base_managed_extension_service_1 = require("../../../modules/extensions/services/base-managed-extension.service");
27
31
  const platform_constants_1 = require("../../../modules/platform/platform.constants");
@@ -31,9 +35,9 @@ const config_model_1 = require("../models/config.model");
31
35
  const remote_access_tailscale_constants_1 = require("../remote-access-tailscale.constants");
32
36
  const remote_access_tailscale_exceptions_1 = require("../remote-access-tailscale.exceptions");
33
37
  const tailscale_cli_service_1 = require("./tailscale-cli.service");
38
+ const tailscale_operation_coordinator_service_1 = require("./tailscale-operation-coordinator.service");
34
39
  const tailscale_serve_service_1 = require("./tailscale-serve.service");
35
40
  const tailscale_status_mapper_service_1 = require("./tailscale-status-mapper.service");
36
- const TAILSCALE_REQUIREMENTS_PERIODIC_REFRESH_MS = 5 * 60 * 1000;
37
41
  const TAILSCALE_PRINT_PLAN_TIMEOUT_MS = 2_000;
38
42
  const OPERATOR_GRANTED_SATISFIED_MESSAGE = 'The smart-panel operator is granted.';
39
43
  const TAILSCALE_DOCUMENTATION_URL = 'https://smart-panel.fastybird.com/docs';
@@ -52,7 +56,7 @@ function compareTailscaleVersions(a, b) {
52
56
  }
53
57
  const UNEVALUATED_MESSAGE = 'Not evaluated: the platform requirement is not satisfied.';
54
58
  let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base_managed_extension_service_1.BaseManagedExtensionService {
55
- constructor(cli, mapper, configService, nestConfigService, platformService, eventEmitter, serveService) {
59
+ constructor(cli, mapper, configService, nestConfigService, platformService, eventEmitter, serveService, operations = new tailscale_operation_coordinator_service_1.TailscaleOperationCoordinatorService()) {
56
60
  super();
57
61
  this.cli = cli;
58
62
  this.mapper = mapper;
@@ -61,22 +65,121 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
61
65
  this.platformService = platformService;
62
66
  this.eventEmitter = eventEmitter;
63
67
  this.serveService = serveService;
68
+ this.operations = operations;
64
69
  this.logger = (0, logger_1.createExtensionLogger)(remote_access_tailscale_constants_1.REMOTE_ACCESS_TAILSCALE_PLUGIN_NAME, 'TailscaleNodeManagedService');
65
70
  this.owner = { kind: 'plugin', type: remote_access_tailscale_constants_1.REMOTE_ACCESS_TAILSCALE_PLUGIN_NAME };
66
71
  this.serviceId = 'node';
67
72
  this.activationPolicy = 'owner-enabled';
73
+ this.observations = new remote_access_observation_utils_1.RemoteAccessObservation();
74
+ this.requirementObservations = new remote_access_observation_utils_1.RemoteAccessObservation();
75
+ this.authentication = 'unknown';
76
+ this.identity = {};
77
+ this.connectPromise = null;
78
+ this.stopPromise = null;
68
79
  this.pollTimer = null;
69
80
  this.lastStatus = null;
70
81
  this.lastError = null;
71
82
  this.pluginConfig = null;
72
83
  this.requirementsCache = null;
73
- this.requirementsRefreshedAt = 0;
74
84
  this.lastServeConvergeDenied = false;
75
85
  this.reconnectAttempts = 0;
76
86
  this.nextReconnectAttemptAt = 0;
87
+ this.operations.onSettled((operation) => {
88
+ if (operation !== 'serve' && operation !== 'reconnect') {
89
+ this.schedulePoll(0);
90
+ }
91
+ });
92
+ }
93
+ getOperationCoordinator() {
94
+ return this.operations;
95
+ }
96
+ getControlState() {
97
+ return {
98
+ enabled: this.isPluginEnabled(),
99
+ serviceState: this.state,
100
+ authentication: this.authentication,
101
+ operation: this.operations.getOperation(),
102
+ };
103
+ }
104
+ isPluginEnabled() {
105
+ try {
106
+ return this.configService.getPluginConfig(remote_access_tailscale_constants_1.REMOTE_ACCESS_TAILSCALE_PLUGIN_NAME).enabled;
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ }
112
+ clearAuthentication() {
113
+ this.authentication = 'required';
114
+ this.identity = {};
115
+ }
116
+ observeAuthentication(status) {
117
+ if (status.BackendState === 'NeedsLogin') {
118
+ this.clearAuthentication();
119
+ }
120
+ else if (['Running', 'Starting', 'Stopped', 'NeedsMachineAuth'].includes(status.BackendState)) {
121
+ this.authentication = 'authenticated';
122
+ this.identity = this.mapper.map(status, { port: this.getBackendPort() }).details;
123
+ }
124
+ }
125
+ async onModuleDestroy() {
126
+ await this.stop();
127
+ }
128
+ async ensureSupervisorStartedForLogin() {
129
+ if (!this.isPluginEnabled()) {
130
+ throw new remote_access_tailscale_exceptions_1.TailscalePluginDisabledException();
131
+ }
132
+ if (this.state !== 'started') {
133
+ await this.startNode(false);
134
+ }
135
+ }
136
+ assertRequirements(requirements) {
137
+ const failed = requirements.find((requirement) => !requirement.satisfied);
138
+ if (failed) {
139
+ throw new remote_access_tailscale_exceptions_1.TailscaleRequirementUnsatisfiedException(failed);
140
+ }
141
+ if (requirements.length === 0) {
142
+ throw new tailscale_cli_service_1.TailscaleCliError('unknown', 'Tailscale setup could not be verified. Re-check setup before connecting.');
143
+ }
144
+ }
145
+ async connect() {
146
+ if (!this.isPluginEnabled()) {
147
+ throw new remote_access_tailscale_exceptions_1.TailscalePluginDisabledException();
148
+ }
149
+ if (this.connectPromise !== null) {
150
+ return this.connectPromise;
151
+ }
152
+ this.connectPromise = (this.state === 'started'
153
+ ? this.operations.run('connect', async (token) => {
154
+ const config = this.getPluginConfig();
155
+ const requirements = await this.refreshRequirements('start');
156
+ this.operations.assertCurrent(token);
157
+ this.assertRequirements(requirements);
158
+ const status = await this.cli.getStatus();
159
+ this.operations.assertCurrent(token);
160
+ this.observeAuthentication(status);
161
+ if (!this.mapper.hasExistingKey(status)) {
162
+ throw new tailscale_cli_service_1.TailscaleCliError('needs-login', 'Sign in to Tailscale before connecting.');
163
+ }
164
+ if (this.mapper.map(status, { port: this.getBackendPort() }).state === 'connected') {
165
+ return;
166
+ }
167
+ await this.cli.set(this.buildPreferenceFlags(config));
168
+ this.operations.assertCurrent(token);
169
+ await this.cli.up(this.buildUpFlags(config));
170
+ this.operations.assertCurrent(token);
171
+ this.schedulePoll(0);
172
+ })
173
+ : this.startNode(true)).finally(() => {
174
+ this.connectPromise = null;
175
+ });
176
+ return this.connectPromise;
77
177
  }
78
178
  async start() {
79
- await this.withLock(async () => {
179
+ return this.startNode(false);
180
+ }
181
+ async startNode(strict) {
182
+ await this.operations.run('connect', async (token) => {
80
183
  if (this.state === 'started') {
81
184
  return;
82
185
  }
@@ -88,15 +191,31 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
88
191
  this.logger.log('Starting Tailscale node service');
89
192
  try {
90
193
  const requirements = await this.refreshRequirements('start');
194
+ this.operations.assertCurrent(token);
195
+ if (strict) {
196
+ this.assertRequirements(requirements);
197
+ }
91
198
  if (this.requirementsSatisfied(requirements)) {
92
199
  const status = await this.getStatusOrNull();
200
+ this.operations.assertCurrent(token);
201
+ if (status) {
202
+ this.observeAuthentication(status);
203
+ }
93
204
  if (status && this.mapper.hasExistingKey(status)) {
94
205
  await this.cli.set(this.buildPreferenceFlags(config));
206
+ this.operations.assertCurrent(token);
95
207
  await this.cli.up(this.buildUpFlags(config));
96
208
  }
97
209
  }
98
210
  }
99
211
  catch (error) {
212
+ this.operations.assertCurrent(token);
213
+ if (strict) {
214
+ this.state = 'error';
215
+ this.lastError = error instanceof Error ? error.message : 'Unknown Tailscale failure.';
216
+ await this.emitStatus();
217
+ throw error;
218
+ }
100
219
  if (error instanceof tailscale_cli_service_1.TailscaleCliError && error.kind === 'permission-denied') {
101
220
  await this.refreshRequirements('permission-denied').catch(() => undefined);
102
221
  }
@@ -109,41 +228,65 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
109
228
  });
110
229
  });
111
230
  }
112
- this.schedulePoll(0);
231
+ this.operations.assertCurrent(token);
113
232
  this.state = 'started';
233
+ this.lastError = null;
234
+ this.schedulePoll(0);
114
235
  this.logger.log('Tailscale node service started');
115
236
  });
237
+ if (strict && this.authentication !== 'authenticated') {
238
+ throw new tailscale_cli_service_1.TailscaleCliError('needs-login', 'Sign in to Tailscale before connecting.');
239
+ }
116
240
  }
117
241
  async stop() {
118
- await this.withLock(async () => {
119
- if (this.state === 'stopped') {
242
+ if (this.stopPromise !== null) {
243
+ return this.stopPromise;
244
+ }
245
+ const shouldDown = this.state !== 'stopped' || this.operations.getOperation() !== null || this.operations.hasPendingChildren();
246
+ this.state = 'stopping';
247
+ this.observations.invalidate();
248
+ this.requirementObservations.invalidate();
249
+ this.clearPoll();
250
+ this.stopPromise = this.operations
251
+ .interrupt('disconnect', async (token) => {
252
+ if (!shouldDown) {
253
+ this.state = 'stopped';
254
+ await this.emitStatus();
120
255
  return;
121
256
  }
122
- this.state = 'stopping';
123
- this.clearPoll();
124
257
  try {
125
258
  await this.cli.down();
259
+ this.operations.assertCurrent(token);
126
260
  this.state = 'stopped';
127
261
  }
128
262
  catch (error) {
129
- if (await this.isStopFailureTolerated(error)) {
130
- this.logger.debug('tailscale down failed while stopping (safe to ignore if the node was already down)', {
131
- message: error instanceof Error ? error.message : String(error),
132
- });
133
- this.state = 'stopped';
134
- }
135
- else {
136
- const message = error instanceof Error ? error.message : String(error);
137
- this.state = 'error';
138
- this.lastError = message;
139
- this.logger.error('Failed to stop the Tailscale node service', { message });
140
- await this.emitStatus();
141
- throw new remote_access_tailscale_exceptions_1.TailscaleNodeStopFailedException(message);
263
+ this.operations.assertCurrent(token);
264
+ const tolerated = await this.isStopFailureTolerated(error);
265
+ this.operations.assertCurrent(token);
266
+ if (!tolerated) {
267
+ throw error;
142
268
  }
269
+ this.state = 'stopped';
270
+ }
271
+ this.lastError = null;
272
+ await this.emitStatus();
273
+ })
274
+ .catch(async (error) => {
275
+ if (error instanceof remote_access_tailscale_exceptions_1.TailscaleOperationCancelledException) {
276
+ throw error;
143
277
  }
278
+ this.state = 'error';
279
+ this.lastError = error instanceof Error ? error.message : 'Unknown Tailscale failure.';
144
280
  await this.emitStatus();
145
- this.logger.log('Tailscale node service stopped');
281
+ if (error instanceof Error && 'code' in error) {
282
+ throw error;
283
+ }
284
+ throw new remote_access_tailscale_exceptions_1.TailscaleNodeStopFailedException(this.lastError);
285
+ })
286
+ .finally(() => {
287
+ this.stopPromise = null;
146
288
  });
289
+ return this.stopPromise;
147
290
  }
148
291
  async isStopFailureTolerated(error) {
149
292
  if (error instanceof tailscale_cli_service_1.TailscaleCliError &&
@@ -158,8 +301,10 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
158
301
  try {
159
302
  return await Promise.race([
160
303
  this.getStatusOrNull(),
161
- new Promise((resolve) => {
162
- timer = setTimeout(() => resolve(null), timeoutMs);
304
+ new Promise((resolve, reject) => {
305
+ timer = setTimeout(() => {
306
+ void this.operations.reapPendingChildren().then(() => resolve(null), reject);
307
+ }, timeoutMs);
163
308
  }),
164
309
  ]);
165
310
  }
@@ -170,14 +315,65 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
170
315
  }
171
316
  }
172
317
  async emitStatus() {
173
- const status = await this.computeStatus();
318
+ const generation = this.operations.getGeneration();
319
+ const status = this.state === 'stopped' || this.state === 'stopping' || this.state === 'error'
320
+ ? this.buildStatus(this.state === 'error' ? 'error' : 'disconnected', this.lastError ?? 'The node service is stopped.', this.identity)
321
+ : await this.computeStatus();
322
+ if (!this.operations.isCurrent(generation)) {
323
+ return;
324
+ }
325
+ this.operations.assertCurrent();
174
326
  this.lastStatus = status;
175
- this.eventEmitter.emit(remote_access_constants_1.EventType.PROVIDER_STATUS, status);
327
+ this.eventEmitter.emit(remote_access_constants_1.EventType.PROVIDER_OBSERVATION, {
328
+ status,
329
+ metadata: { requirements: this.getRequirements(), control: this.getControlState() },
330
+ });
176
331
  }
177
332
  async onConfigChanged() {
333
+ this.observations.invalidate();
334
+ this.requirementObservations.invalidate();
335
+ if (this.stopPromise !== null) {
336
+ await this.stopPromise;
337
+ }
338
+ this.clearPoll();
339
+ return this.operations.interrupt('config', async (token) => {
340
+ let result;
341
+ try {
342
+ result = await this.applyConfigChange();
343
+ }
344
+ catch (error) {
345
+ this.operations.assertCurrent(token);
346
+ this.state = 'error';
347
+ this.lastError = error instanceof Error ? error.message : 'Unknown Tailscale failure.';
348
+ await this.emitStatus();
349
+ throw error;
350
+ }
351
+ this.operations.assertCurrent(token);
352
+ if (this.isPollable()) {
353
+ this.schedulePoll(0);
354
+ }
355
+ return result;
356
+ });
357
+ }
358
+ async applyConfigChange() {
178
359
  const previous = this.pluginConfig;
179
360
  this.pluginConfig = null;
180
361
  const next = this.getPluginConfig();
362
+ if (!next.enabled) {
363
+ this.state = 'stopping';
364
+ try {
365
+ await this.cli.down();
366
+ }
367
+ catch (error) {
368
+ if (!(await this.isStopFailureTolerated(error))) {
369
+ throw error;
370
+ }
371
+ }
372
+ this.operations.assertCurrent();
373
+ this.state = 'stopped';
374
+ await this.emitStatus();
375
+ return { restartRequired: false };
376
+ }
181
377
  if (!previous || previous.loginServer !== next.loginServer) {
182
378
  this.logger.log('Tailscale login_server changed (or its prior value was unknown), restart required');
183
379
  try {
@@ -194,14 +390,21 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
194
390
  throw error;
195
391
  }
196
392
  }
393
+ this.operations.assertCurrent();
394
+ this.clearAuthentication();
197
395
  return { restartRequired: true };
198
396
  }
397
+ if (!this.isPollable()) {
398
+ return { restartRequired: false };
399
+ }
199
400
  try {
200
401
  const requirements = await this.refreshRequirements('start');
201
402
  if (this.requirementsSatisfied(requirements)) {
202
403
  const status = await this.getStatusOrNull();
203
404
  if (status && this.mapper.hasExistingKey(status)) {
405
+ this.operations.assertCurrent();
204
406
  await this.cli.set(this.buildPreferenceFlags(next));
407
+ this.operations.assertCurrent();
205
408
  const port = this.getBackendPort();
206
409
  if (this.mapper.map(status, { port }).state === 'connected') {
207
410
  await this.convergeServe(next, port, status);
@@ -210,6 +413,7 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
210
413
  }
211
414
  }
212
415
  catch (error) {
416
+ this.operations.assertCurrent();
213
417
  if (error instanceof tailscale_cli_service_1.TailscaleCliError && error.kind === 'permission-denied') {
214
418
  await this.refreshRequirements('permission-denied').catch(() => undefined);
215
419
  }
@@ -220,52 +424,114 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
220
424
  return { restartRequired: false };
221
425
  }
222
426
  async isHealthy() {
427
+ const generation = this.operations.getGeneration();
428
+ if (!this.isPollable()) {
429
+ return false;
430
+ }
223
431
  if (!this.requirementsSatisfied(this.getRequirements())) {
224
432
  return false;
225
433
  }
226
434
  try {
227
- const status = await this.cli.getStatus();
228
- return status.BackendState === 'Running' && status.Self?.Online === true;
435
+ const { raw: status } = await this.computeStatusWithRawStatus();
436
+ return (this.isPollable() &&
437
+ this.operations.isCurrent(generation) &&
438
+ status?.BackendState === 'Running' &&
439
+ status.Self?.Online === true);
229
440
  }
230
441
  catch {
231
442
  return false;
232
443
  }
233
444
  }
234
445
  async factoryReset() {
235
- await this.cli.serveReset().catch(() => undefined);
446
+ this.state = 'stopping';
447
+ this.observations.invalidate();
448
+ this.requirementObservations.invalidate();
449
+ this.clearPoll();
236
450
  try {
237
- await this.cli.logout();
238
- return { success: true };
451
+ return await this.operations.interrupt('reset', async (token) => {
452
+ await this.cli.down().catch((error) => this.isStopFailureTolerated(error).then((safe) => {
453
+ if (!safe) {
454
+ throw error;
455
+ }
456
+ }));
457
+ this.operations.assertCurrent(token);
458
+ await this.cli.serveReset().catch(() => undefined);
459
+ this.operations.assertCurrent(token);
460
+ await this.cli.logout().catch((error) => {
461
+ if (!(error instanceof tailscale_cli_service_1.TailscaleCliError && ['not-installed', 'needs-login', 'daemon-down'].includes(error.kind))) {
462
+ throw error;
463
+ }
464
+ });
465
+ this.operations.assertCurrent(token);
466
+ this.clearAuthentication();
467
+ this.state = 'stopped';
468
+ await this.emitStatus();
469
+ return { success: true };
470
+ });
239
471
  }
240
472
  catch (error) {
241
- if (error instanceof tailscale_cli_service_1.TailscaleCliError && (error.kind === 'not-installed' || error.kind === 'needs-login')) {
242
- return { success: true };
473
+ if (!(error instanceof remote_access_tailscale_exceptions_1.TailscaleOperationCancelledException)) {
474
+ this.state = 'error';
475
+ this.lastError = error instanceof Error ? error.message : 'Unknown error';
476
+ await this.emitStatus();
243
477
  }
244
478
  return { success: false, reason: error instanceof Error ? error.message : 'Unknown error' };
245
479
  }
246
480
  }
247
481
  async evaluateRequirements() {
248
- return this.refreshRequirements('status-read');
482
+ return this.refreshRequirements('setup-complete');
483
+ }
484
+ async reconcileSetup(generation) {
485
+ if (!this.operations.isCurrent(generation) || !this.getControlState().enabled) {
486
+ return;
487
+ }
488
+ await this.operations.run('config', async (token) => {
489
+ this.operations.assertCurrent(token);
490
+ if (!this.operations.isCurrent(generation)) {
491
+ return;
492
+ }
493
+ if (this.state !== 'started') {
494
+ await this.startNode(false);
495
+ }
496
+ else {
497
+ const status = await this.cli.getStatus();
498
+ this.operations.assertCurrent(token);
499
+ this.observeAuthentication(status);
500
+ if (this.mapper.hasExistingKey(status)) {
501
+ await this.cli.set(this.buildPreferenceFlags(this.getPluginConfig()));
502
+ this.operations.assertCurrent(token);
503
+ await this.cli.up(this.buildUpFlags(this.getPluginConfig()));
504
+ this.operations.assertCurrent(token);
505
+ }
506
+ this.schedulePoll(0);
507
+ }
508
+ });
249
509
  }
250
510
  getRequirements() {
251
511
  return this.requirementsCache ?? [];
252
512
  }
253
- async refreshRequirements(reason) {
254
- if (reason === 'periodic' &&
255
- this.requirementsCache &&
256
- Date.now() - this.requirementsRefreshedAt < TAILSCALE_REQUIREMENTS_PERIODIC_REFRESH_MS) {
257
- return this.requirementsCache;
513
+ async refreshRequirements(reason, signal) {
514
+ const generation = this.operations.getGeneration();
515
+ const readOnly = reason === 'periodic' || reason === 'status-read';
516
+ const requirements = await (readOnly
517
+ ? this.requirementObservations.run((readSignal) => this.evaluateRequirementsLive(generation, false, readSignal), signal)
518
+ : this.evaluateRequirementsLive(generation, true, signal));
519
+ signal?.throwIfAborted();
520
+ if (!this.operations.isCurrent(generation)) {
521
+ return this.getRequirements();
258
522
  }
259
- const requirements = await this.evaluateRequirementsLive();
260
523
  this.requirementsCache = requirements;
261
- this.requirementsRefreshedAt = Date.now();
262
524
  return requirements;
263
525
  }
264
526
  requirementsSatisfied(requirements) {
265
527
  return requirements.length > 0 && requirements.every((requirement) => requirement.satisfied);
266
528
  }
267
- async evaluateRequirementsLive() {
529
+ async evaluateRequirementsLive(generation, allowProbe = false, signal) {
268
530
  const platform = await this.evaluatePlatformSupported();
531
+ signal?.throwIfAborted();
532
+ if (!this.operations.isCurrent(generation)) {
533
+ return this.getRequirements();
534
+ }
269
535
  if (!platform.satisfied) {
270
536
  return [
271
537
  await this.finalizeRequirement(platform),
@@ -275,20 +541,34 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
275
541
  this.unevaluatedRequirement('version-supported'),
276
542
  ];
277
543
  }
278
- const [{ binary, version }, daemonActive, operatorGranted] = await Promise.all([
279
- this.evaluateBinaryAndVersion(),
280
- this.evaluateDaemonActive(),
281
- this.evaluateOperatorGranted(),
544
+ const results = await Promise.allSettled([
545
+ this.evaluateBinaryAndVersion(signal),
546
+ this.evaluateDaemonActive(signal),
547
+ this.evaluateOperatorGranted(generation, allowProbe, signal),
282
548
  ]);
283
- return this.attachRemedies([platform, binary, daemonActive, operatorGranted, version]);
549
+ signal?.throwIfAborted();
550
+ const [binaryResult, daemonResult, operatorResult] = results;
551
+ if (binaryResult.status === 'rejected') {
552
+ throw binaryResult.reason;
553
+ }
554
+ if (daemonResult.status === 'rejected') {
555
+ throw daemonResult.reason;
556
+ }
557
+ if (operatorResult.status === 'rejected') {
558
+ throw operatorResult.reason;
559
+ }
560
+ const { binary, version } = binaryResult.value;
561
+ const daemonActive = daemonResult.value;
562
+ const operatorGranted = operatorResult.value;
563
+ return this.attachRemedies([platform, binary, daemonActive, operatorGranted, version], signal);
284
564
  }
285
565
  unevaluatedRequirement(code) {
286
566
  return { code, satisfied: false, message: UNEVALUATED_MESSAGE, remedy: null };
287
567
  }
288
- async attachRemedies(requirements) {
568
+ async attachRemedies(requirements, signal) {
289
569
  let installRemedyPromise = null;
290
570
  const getInstallRemedy = () => {
291
- installRemedyPromise ??= this.buildInstallRemedy();
571
+ installRemedyPromise ??= this.buildInstallRemedy(signal);
292
572
  return installRemedyPromise;
293
573
  };
294
574
  return Promise.all(requirements.map((requirement) => this.finalizeRequirement(requirement, getInstallRemedy)));
@@ -317,9 +597,9 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
317
597
  return { commands: [], note: TAILSCALE_DOCUMENTATION_URL };
318
598
  }
319
599
  }
320
- async buildInstallRemedy() {
600
+ async buildInstallRemedy(signal) {
321
601
  try {
322
- const stdout = await this.runSetupScriptPrintPlan('install');
602
+ const stdout = await this.runSetupScriptPrintPlan(signal);
323
603
  const lines = stdout
324
604
  .split('\n')
325
605
  .map((line) => line.trim())
@@ -339,37 +619,100 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
339
619
  return { commands: [], note: TAILSCALE_VENDOR_DOWNLOAD_URL };
340
620
  }
341
621
  }
342
- runSetupScriptPrintPlan(step) {
622
+ async runSetupScriptPrintPlan(signal) {
343
623
  const script = (0, path_1.join)(__dirname, '..', 'scripts', 'tailscale-setup.sh');
344
- return new Promise((resolve, reject) => {
345
- (0, node_child_process_1.execFile)('bash', [script, '--print-plan', `--step=${step}`], { timeout: TAILSCALE_PRINT_PLAN_TIMEOUT_MS }, (error, stdout) => {
346
- if (error) {
347
- reject(error);
348
- return;
349
- }
350
- resolve(stdout ?? '');
351
- });
352
- });
624
+ return (await (0, cancellable_exec_utils_1.cancellableExecFile)('bash', [script, '--print-plan', '--step=install'], signal, TAILSCALE_PRINT_PLAN_TIMEOUT_MS)).stdout;
625
+ }
626
+ async computeStatus(_requirementsReason = 'status-read', options) {
627
+ return (await this.computeStatusWithRawStatus(options)).status;
628
+ }
629
+ async awaitObservationIdle() {
630
+ await Promise.all([this.observations.awaitIdle(), this.requirementObservations.awaitIdle()]);
631
+ }
632
+ async getStatusSnapshot(options) {
633
+ const observation = await this.computeStatusWithRawStatus(options);
634
+ return {
635
+ status: observation.status,
636
+ metadata: { requirements: observation.requirements, control: this.getControlState() },
637
+ };
638
+ }
639
+ async computeStatusWithRawStatus(options) {
640
+ const generation = this.operations.getGeneration();
641
+ const revision = this.operations.getObservationRevision();
642
+ try {
643
+ const result = await this.observations.run(async (signal) => {
644
+ const requirements = await this.refreshRequirements('status-read', signal);
645
+ const collected = await this.collectStatusWithRawStatus(generation, signal, requirements);
646
+ signal.throwIfAborted();
647
+ return { ...collected, requirements };
648
+ }, options?.signal);
649
+ if (!this.operations.isCurrent(generation) || !this.operations.isObservationCurrent(revision)) {
650
+ return this.supersededObservation();
651
+ }
652
+ if (result.raw && this.isPollable()) {
653
+ this.observeAuthentication(result.raw);
654
+ }
655
+ return result;
656
+ }
657
+ catch (error) {
658
+ if (!this.operations.isCurrent(generation) ||
659
+ !this.operations.isObservationCurrent(revision) ||
660
+ !this.isPollable()) {
661
+ return this.supersededObservation();
662
+ }
663
+ return {
664
+ status: this.buildStatus('error', error instanceof Error ? error.message : 'The status observation failed.'),
665
+ raw: null,
666
+ requirements: this.getRequirements(),
667
+ };
668
+ }
353
669
  }
354
- async computeStatus(requirementsReason = 'status-read') {
355
- return (await this.computeStatusWithRawStatus(requirementsReason)).status;
670
+ supersededObservation() {
671
+ return {
672
+ status: this.buildStatus(this.state === 'error' ? 'error' : 'disconnected', this.lastError ?? 'The observation was superseded by a lifecycle action.'),
673
+ raw: null,
674
+ requirements: this.getRequirements(),
675
+ };
356
676
  }
357
- async computeStatusWithRawStatus(requirementsReason = 'status-read') {
677
+ async collectStatusWithRawStatus(generation = this.operations.getGeneration(), signal, requirements = this.getRequirements()) {
358
678
  if (this.state === 'stopped' || this.state === 'stopping') {
359
- return { status: this.buildStatus('disconnected', 'The node service is stopped.'), raw: null };
679
+ return { status: this.buildStatus('disconnected', 'The node service is stopped.', this.identity), raw: null };
360
680
  }
361
681
  if (this.state === 'error') {
362
682
  return { status: this.buildStatus('error', this.lastError ?? 'The Tailscale node service failed.'), raw: null };
363
683
  }
364
- const platform = await this.evaluatePlatformSupported();
684
+ const platform = requirements.find((requirement) => requirement.code === 'platform-supported') ?? {
685
+ satisfied: false,
686
+ message: 'Platform detection did not complete.',
687
+ };
688
+ signal?.throwIfAborted();
689
+ if (!this.operations.isCurrent(generation)) {
690
+ return {
691
+ status: this.buildStatus('disconnected', 'The observation was superseded by a lifecycle action.', this.identity),
692
+ raw: null,
693
+ };
694
+ }
365
695
  if (!platform.satisfied) {
366
696
  return { status: this.buildStatus('unsupported', platform.message), raw: null };
367
697
  }
368
698
  try {
369
- const status = await this.cli.getStatus();
699
+ const status = await this.cli.getStatus(signal);
700
+ signal?.throwIfAborted();
701
+ if (!this.operations.isCurrent(generation)) {
702
+ return {
703
+ status: this.buildStatus('disconnected', 'The observation was superseded by a lifecycle action.', this.identity),
704
+ raw: null,
705
+ };
706
+ }
370
707
  const port = this.getBackendPort();
371
708
  const postureAdvisories = this.buildPostureAdvisories(status);
372
- const requirements = await this.refreshRequirements(requirementsReason);
709
+ signal?.throwIfAborted();
710
+ if (!this.operations.isCurrent(generation)) {
711
+ return {
712
+ status: this.buildStatus('disconnected', 'The observation was superseded by a lifecycle action.', this.identity),
713
+ raw: null,
714
+ };
715
+ }
373
716
  const operatorRequirement = requirements.find((requirement) => requirement.code === 'operator-granted');
374
717
  if (operatorRequirement && !operatorRequirement.satisfied) {
375
718
  return {
@@ -388,7 +731,7 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
388
731
  };
389
732
  }
390
733
  const config = this.getPluginConfig();
391
- const serveResult = await this.serveService.read(config, port, status);
734
+ const serveResult = await this.serveService.read(config, port, status, signal);
392
735
  return {
393
736
  status: this.buildStatus(mapped.state, mapped.message, mapped.details, [...mapped.proxyAddresses, ...serveResult.proxyAddresses], [...mapped.endpoints, ...serveResult.endpoints], [...postureAdvisories, ...serveResult.advisories]),
394
737
  raw: status,
@@ -414,7 +757,11 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
414
757
  }
415
758
  }
416
759
  async convergeServe(config, port, status) {
417
- const result = await this.serveService.converge(config, port, status);
760
+ const result = await this.operations.run('serve', async (token) => {
761
+ const converged = await this.serveService.converge(config, port, status);
762
+ this.operations.assertCurrent(token);
763
+ return converged;
764
+ }, false);
418
765
  if (result.permissionDenied) {
419
766
  if (!this.lastServeConvergeDenied) {
420
767
  this.lastServeConvergeDenied = true;
@@ -453,9 +800,9 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
453
800
  message: `Tailscale is not supported on the '${platformType}' platform.`,
454
801
  };
455
802
  }
456
- async evaluateBinaryAndVersion() {
803
+ async evaluateBinaryAndVersion(signal) {
457
804
  try {
458
- const info = await this.cli.getVersion();
805
+ const info = await this.cli.getVersion(signal);
459
806
  const supported = compareTailscaleVersions(info.version, remote_access_tailscale_constants_1.TAILSCALE_MIN_VERSION) >= 0;
460
807
  return {
461
808
  binary: { code: 'binary-installed', satisfied: true, message: `Tailscale ${info.version} is installed.` },
@@ -484,21 +831,24 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
484
831
  };
485
832
  }
486
833
  }
487
- async evaluateDaemonActive() {
488
- const active = await this.isSystemdUnitActive('tailscaled');
834
+ async evaluateDaemonActive(signal) {
835
+ const active = await this.isSystemdUnitActive('tailscaled', signal);
489
836
  return {
490
837
  code: 'daemon-active',
491
838
  satisfied: active,
492
839
  message: active ? 'tailscaled is active.' : 'tailscaled is not active. Run setup or start the service.',
493
840
  };
494
841
  }
495
- async evaluateOperatorGranted() {
842
+ async evaluateOperatorGranted(generation = this.operations.getGeneration(), allowProbe = false, signal) {
496
843
  const serviceUser = os_1.default.userInfo().username;
497
844
  try {
498
- const prefs = await this.cli.getPrefs();
845
+ const prefs = await this.cli.getPrefs(signal);
499
846
  return this.buildOperatorRequirement(prefs.OperatorUser === serviceUser, serviceUser);
500
847
  }
501
848
  catch (error) {
849
+ if (!this.operations.isCurrent(generation)) {
850
+ return { code: 'operator-granted', satisfied: false, message: 'The requirement observation was superseded.' };
851
+ }
502
852
  if (error instanceof tailscale_cli_service_1.TailscaleCliError) {
503
853
  if (error.kind === 'daemon-down') {
504
854
  return {
@@ -515,12 +865,22 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
515
865
  };
516
866
  }
517
867
  }
868
+ if (!allowProbe) {
869
+ const verified = this.getRequirements().find((requirement) => requirement.code === 'operator-granted');
870
+ return verified?.satisfied
871
+ ? verified
872
+ : {
873
+ code: 'operator-granted',
874
+ satisfied: false,
875
+ message: 'The operator grant cannot be verified by this Tailscale version. Run Set up or Connect to verify management access.',
876
+ };
877
+ }
518
878
  return this.evaluateOperatorGrantedViaProbe(serviceUser);
519
879
  }
520
880
  }
521
881
  async evaluateOperatorGrantedViaProbe(serviceUser) {
522
882
  try {
523
- await this.cli.set([`--operator=${serviceUser}`]);
883
+ await this.operations.run('preferences', async () => this.cli.set([`--operator=${serviceUser}`]), false);
524
884
  return this.buildOperatorRequirement(true, serviceUser);
525
885
  }
526
886
  catch (error) {
@@ -563,16 +923,15 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
563
923
  return null;
564
924
  }
565
925
  }
566
- isSystemdUnitActive(unit) {
567
- return new Promise((resolve) => {
568
- (0, node_child_process_1.execFile)('systemctl', ['is-active', unit], { timeout: remote_access_tailscale_constants_1.TAILSCALE_SYSTEMCTL_PROBE_TIMEOUT_MS }, (error, stdout) => {
569
- if (error) {
570
- resolve(false);
571
- return;
572
- }
573
- resolve((stdout ?? '').trim() === 'active');
574
- });
575
- });
926
+ async isSystemdUnitActive(unit, signal) {
927
+ try {
928
+ const result = await (0, cancellable_exec_utils_1.cancellableExecFile)('systemctl', ['is-active', unit], signal, remote_access_tailscale_constants_1.TAILSCALE_SYSTEMCTL_PROBE_TIMEOUT_MS);
929
+ return result.exitCode === 0 && result.stdout.trim() === 'active';
930
+ }
931
+ catch {
932
+ signal?.throwIfAborted();
933
+ return false;
934
+ }
576
935
  }
577
936
  isPollable() {
578
937
  return this.state === 'starting' || this.state === 'started';
@@ -583,7 +942,7 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
583
942
  return;
584
943
  }
585
944
  this.pollTimer = setTimeout(() => {
586
- void this.pollTick();
945
+ this.operations.withoutOperation(() => void this.pollTick());
587
946
  }, delayMs);
588
947
  this.pollTimer.unref?.();
589
948
  }
@@ -594,9 +953,13 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
594
953
  }
595
954
  }
596
955
  async pollTick() {
956
+ const generation = this.operations.getGeneration();
957
+ let revision = this.operations.getObservationRevision();
597
958
  try {
598
- let { status, raw } = await this.computeStatusWithRawStatus('periodic');
599
- if (!this.isPollable()) {
959
+ let { status, raw, requirements } = await this.computeStatusWithRawStatus();
960
+ if (!this.isPollable() ||
961
+ !this.operations.isCurrent(generation) ||
962
+ !this.operations.isObservationCurrent(revision)) {
600
963
  return;
601
964
  }
602
965
  if (status.state === 'connected') {
@@ -609,27 +972,39 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
609
972
  this.requirementsSatisfied(this.getRequirements()) &&
610
973
  Date.now() >= this.nextReconnectAttemptAt) {
611
974
  const reconnected = await this.attemptReconnect();
612
- if (!this.isPollable()) {
975
+ revision = this.operations.getObservationRevision();
976
+ if (!this.isPollable() || !this.operations.isCurrent(generation)) {
613
977
  return;
614
978
  }
615
979
  if (reconnected) {
616
- ({ status, raw } = await this.computeStatusWithRawStatus('periodic'));
617
- if (!this.isPollable()) {
980
+ ({ status, raw, requirements } = await this.computeStatusWithRawStatus());
981
+ if (!this.isPollable() || !this.operations.isCurrent(generation)) {
618
982
  return;
619
983
  }
620
984
  }
621
985
  }
622
- if (raw && status.state === 'connected' && this.requirementsSatisfied(this.getRequirements())) {
623
- await this.convergeServe(this.getPluginConfig(), this.getBackendPort(), raw);
986
+ if (!this.isPollable() ||
987
+ !this.operations.isCurrent(generation) ||
988
+ !this.operations.isObservationCurrent(revision)) {
989
+ return;
624
990
  }
625
991
  if (this.hasStatusChanged(this.lastStatus, status)) {
626
992
  this.lastStatus = status;
627
- this.eventEmitter.emit(remote_access_constants_1.EventType.PROVIDER_STATUS, status);
993
+ this.eventEmitter.emit(remote_access_constants_1.EventType.PROVIDER_OBSERVATION, {
994
+ status,
995
+ metadata: { requirements, control: this.getControlState() },
996
+ });
997
+ }
998
+ if (raw && status.state === 'connected' && this.requirementsSatisfied(this.getRequirements())) {
999
+ await this.convergeServe(this.getPluginConfig(), this.getBackendPort(), raw);
1000
+ }
1001
+ if (!this.isPollable() || !this.operations.isCurrent(generation)) {
1002
+ return;
628
1003
  }
629
1004
  this.schedulePoll(status.state === 'connecting' ? remote_access_tailscale_constants_1.TAILSCALE_POLL_INTERVAL_TRANSITIONING_MS : remote_access_tailscale_constants_1.TAILSCALE_POLL_INTERVAL_STABLE_MS);
630
1005
  }
631
1006
  catch (error) {
632
- if (!this.isPollable()) {
1007
+ if (!this.isPollable() || !this.operations.isCurrent(generation)) {
633
1008
  return;
634
1009
  }
635
1010
  this.logger.error('Tailscale status poll failed', {
@@ -639,20 +1014,23 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
639
1014
  }
640
1015
  }
641
1016
  async attemptReconnect() {
642
- return this.withLock(async () => {
1017
+ return this.operations.run('reconnect', async (token) => {
643
1018
  if (this.state !== 'started') {
644
1019
  return false;
645
1020
  }
646
1021
  try {
647
1022
  const config = this.getPluginConfig();
648
1023
  await this.cli.set(this.buildPreferenceFlags(config));
1024
+ this.operations.assertCurrent(token);
649
1025
  await this.cli.up(this.buildUpFlags(config));
1026
+ this.operations.assertCurrent(token);
650
1027
  this.reconnectAttempts = 0;
651
1028
  this.nextReconnectAttemptAt = 0;
652
1029
  this.logger.log('Tailscale node reconnected automatically after being found unexpectedly disconnected.');
653
1030
  return true;
654
1031
  }
655
1032
  catch (error) {
1033
+ this.operations.assertCurrent(token);
656
1034
  this.reconnectAttempts += 1;
657
1035
  const delayMs = Math.min(remote_access_tailscale_constants_1.TAILSCALE_RECONNECT_BASE_DELAY_MS * 2 ** (this.reconnectAttempts - 1), remote_access_tailscale_constants_1.TAILSCALE_RECONNECT_MAX_DELAY_MS);
658
1036
  this.nextReconnectAttemptAt = Date.now() + delayMs;
@@ -746,12 +1124,14 @@ let TailscaleNodeManagedService = class TailscaleNodeManagedService extends base
746
1124
  exports.TailscaleNodeManagedService = TailscaleNodeManagedService;
747
1125
  exports.TailscaleNodeManagedService = TailscaleNodeManagedService = __decorate([
748
1126
  (0, common_1.Injectable)(),
1127
+ __param(7, (0, common_1.Optional)()),
749
1128
  __metadata("design:paramtypes", [tailscale_cli_service_1.TailscaleCliService,
750
1129
  tailscale_status_mapper_service_1.TailscaleStatusMapperService,
751
1130
  config_service_1.ConfigService,
752
1131
  config_1.ConfigService,
753
1132
  platform_service_1.PlatformService,
754
1133
  event_emitter_1.EventEmitter2,
755
- tailscale_serve_service_1.TailscaleServeService])
1134
+ tailscale_serve_service_1.TailscaleServeService,
1135
+ tailscale_operation_coordinator_service_1.TailscaleOperationCoordinatorService])
756
1136
  ], TailscaleNodeManagedService);
757
1137
  //# sourceMappingURL=tailscale-node-managed.service.js.map