dsh-builtin-browser 0.4.10 → 0.4.11

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.
@@ -699,11 +699,38 @@ export class SystemBrowserViewHost {
699
699
  // The port file is deliberately NOT deleted here, and that is a reversal.
700
700
  //
701
701
  // It used to be removed so the launch loop below could not read a previous browser's port. But
702
- // the loop already refuses any file older than the spawn (`freshPort` compares mtimes), so the
703
- // deletion never protected it — while it DID destroy the only record of a browser that may
704
- // still be running for this profile. That record is what `adoptRunningBrowser` needs when the
705
- // launch comes back as a hand-off: the browser was alive the whole time, and without the file
706
- // there is no way to find it again.
702
+ // the loop judges freshness against a snapshot taken below, so the deletion never protected it
703
+ // — while it DID destroy the only record of a browser that may still be running for this
704
+ // profile. That record is what `adoptRunningBrowser` needs when the launch comes back as a
705
+ // hand-off: the browser was alive the whole time, and without the file there is no way to find
706
+ // it again.
707
+ const portFile = join(this.profileDir, 'DevToolsActivePort');
708
+ /**
709
+ * What the port file was like BEFORE this launch, so "fresh" is judged against a snapshot
710
+ * rather than against a clock.
711
+ *
712
+ * The test used to be `mtimeMs < spawnedAt` — a filesystem timestamp compared to `Date.now()`.
713
+ * Those are different clocks with different resolutions, and CI is where it showed: a file the
714
+ * fake browser had just written was rejected as stale, so the loop waited out its full 30s
715
+ * against a browser that was already listening, because the file's mtime had rounded down
716
+ * below the spawn time. That is not a test-only problem: on any filesystem whose mtime
717
+ * resolution is coarser than a millisecond, the plugin would fail to see the browser it had
718
+ * just launched, and report a CDP timeout for a browser that was up the whole time.
719
+ *
720
+ * `undefined` means there was no file, so any file that appears afterwards is fresh. When one
721
+ * existed, a later mtime OR different contents makes the new one fresh. Byte-identical contents
722
+ * with an unchanged mtime is the PREVIOUS browser's port, and adopting it is what this whole
723
+ * check exists to prevent: it may name a port that now belongs to something else entirely.
724
+ */
725
+ const portFileBefore = (() => {
726
+ try {
727
+ return { mtimeMs: statSync(portFile).mtimeMs, contents: readFileSync(portFile, 'utf8') };
728
+ }
729
+ catch {
730
+ return undefined;
731
+ }
732
+ })();
733
+ // Kept for the failure message and for the exit listener's diagnostics; no longer a judgement.
707
734
  const spawnedAt = Date.now();
708
735
  let child;
709
736
  try {
@@ -739,22 +766,28 @@ export class SystemBrowserViewHost {
739
766
  // Clearing these is what makes the restart usable rather than merely alive.
740
767
  this.forgetBrowserState();
741
768
  });
742
- const portFile = join(this.profileDir, 'DevToolsActivePort');
743
769
  /**
744
770
  * The port this launch wrote, or undefined while it has not written one yet.
745
771
  *
746
- * A file left by the PREVIOUS browser is not evidence about this one. Its contents
747
- * name a port that may now belong to something else entirely — another Chromium, a
748
- * node --inspect — and connecting there publishes a client this host does not own and
749
- * dispose() can never kill. Only a file written after the spawn counts.
772
+ * A file left by the PREVIOUS browser is not evidence about this one. Its contents name a port
773
+ * that may now belong to something else entirely — another Chromium, a `node --inspect` — and
774
+ * connecting there publishes a client this host does not own and dispose() can never kill.
775
+ * Only a file that CHANGED across this launch counts, which is what `portFileBefore` records.
750
776
  */
751
777
  const freshPort = () => {
752
778
  try {
753
779
  if (!existsSync(portFile))
754
780
  return undefined;
755
- if (statSync(portFile).mtimeMs < spawnedAt)
781
+ const after = statSync(portFile);
782
+ const contents = readFileSync(portFile, 'utf8');
783
+ // Unchanged since before the launch: this is the previous browser's file, whatever its
784
+ // mtime says. Comparing against the snapshot rather than the spawn time is what keeps this
785
+ // correct on a filesystem whose timestamps are coarser than the clock behind spawnedAt.
786
+ if (portFileBefore !== undefined
787
+ && after.mtimeMs <= portFileBefore.mtimeMs
788
+ && contents === portFileBefore.contents)
756
789
  return undefined;
757
- const value = Number(readFileSync(portFile, 'utf8').split('\n')[0]?.trim() ?? '');
790
+ const value = Number(contents.split('\n')[0]?.trim() ?? '');
758
791
  return Number.isInteger(value) && value > 0 ? value : undefined;
759
792
  }
760
793
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-builtin-browser",
3
- "version": "0.4.10",
3
+ "version": "0.4.11",
4
4
  "description": "Shared real browser plugin for DeepSeek Harness: install-and-use — a visible native browser the human can take over, driven by the agent over CDP. DOM-level interaction (React/Vue safe), per-task session isolation, cookie persistence (dsh_browser_auth), CAPTCHA detection, batch form fill (dsh_browser_fill), authenticated download, history/replay, action restriction",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -721,11 +721,37 @@ export class SystemBrowserViewHost implements ElectronBrowserViewHost {
721
721
  // The port file is deliberately NOT deleted here, and that is a reversal.
722
722
  //
723
723
  // It used to be removed so the launch loop below could not read a previous browser's port. But
724
- // the loop already refuses any file older than the spawn (`freshPort` compares mtimes), so the
725
- // deletion never protected it — while it DID destroy the only record of a browser that may
726
- // still be running for this profile. That record is what `adoptRunningBrowser` needs when the
727
- // launch comes back as a hand-off: the browser was alive the whole time, and without the file
728
- // there is no way to find it again.
724
+ // the loop judges freshness against a snapshot taken below, so the deletion never protected it
725
+ // — while it DID destroy the only record of a browser that may still be running for this
726
+ // profile. That record is what `adoptRunningBrowser` needs when the launch comes back as a
727
+ // hand-off: the browser was alive the whole time, and without the file there is no way to find
728
+ // it again.
729
+ const portFile = join(this.profileDir, 'DevToolsActivePort')
730
+ /**
731
+ * What the port file was like BEFORE this launch, so "fresh" is judged against a snapshot
732
+ * rather than against a clock.
733
+ *
734
+ * The test used to be `mtimeMs < spawnedAt` — a filesystem timestamp compared to `Date.now()`.
735
+ * Those are different clocks with different resolutions, and CI is where it showed: a file the
736
+ * fake browser had just written was rejected as stale, so the loop waited out its full 30s
737
+ * against a browser that was already listening, because the file's mtime had rounded down
738
+ * below the spawn time. That is not a test-only problem: on any filesystem whose mtime
739
+ * resolution is coarser than a millisecond, the plugin would fail to see the browser it had
740
+ * just launched, and report a CDP timeout for a browser that was up the whole time.
741
+ *
742
+ * `undefined` means there was no file, so any file that appears afterwards is fresh. When one
743
+ * existed, a later mtime OR different contents makes the new one fresh. Byte-identical contents
744
+ * with an unchanged mtime is the PREVIOUS browser's port, and adopting it is what this whole
745
+ * check exists to prevent: it may name a port that now belongs to something else entirely.
746
+ */
747
+ const portFileBefore = ((): { mtimeMs: number; contents: string } | undefined => {
748
+ try {
749
+ return { mtimeMs: statSync(portFile).mtimeMs, contents: readFileSync(portFile, 'utf8') }
750
+ } catch {
751
+ return undefined
752
+ }
753
+ })()
754
+ // Kept for the failure message and for the exit listener's diagnostics; no longer a judgement.
729
755
  const spawnedAt = Date.now()
730
756
  let child: ReturnType<typeof spawn>
731
757
  try {
@@ -759,20 +785,26 @@ export class SystemBrowserViewHost implements ElectronBrowserViewHost {
759
785
  // Clearing these is what makes the restart usable rather than merely alive.
760
786
  this.forgetBrowserState()
761
787
  })
762
- const portFile = join(this.profileDir, 'DevToolsActivePort')
763
788
  /**
764
789
  * The port this launch wrote, or undefined while it has not written one yet.
765
790
  *
766
- * A file left by the PREVIOUS browser is not evidence about this one. Its contents
767
- * name a port that may now belong to something else entirely — another Chromium, a
768
- * node --inspect — and connecting there publishes a client this host does not own and
769
- * dispose() can never kill. Only a file written after the spawn counts.
791
+ * A file left by the PREVIOUS browser is not evidence about this one. Its contents name a port
792
+ * that may now belong to something else entirely — another Chromium, a `node --inspect` — and
793
+ * connecting there publishes a client this host does not own and dispose() can never kill.
794
+ * Only a file that CHANGED across this launch counts, which is what `portFileBefore` records.
770
795
  */
771
796
  const freshPort = (): number | undefined => {
772
797
  try {
773
798
  if (!existsSync(portFile)) return undefined
774
- if (statSync(portFile).mtimeMs < spawnedAt) return undefined
775
- const value = Number(readFileSync(portFile, 'utf8').split('\n')[0]?.trim() ?? '')
799
+ const after = statSync(portFile)
800
+ const contents = readFileSync(portFile, 'utf8')
801
+ // Unchanged since before the launch: this is the previous browser's file, whatever its
802
+ // mtime says. Comparing against the snapshot rather than the spawn time is what keeps this
803
+ // correct on a filesystem whose timestamps are coarser than the clock behind spawnedAt.
804
+ if (portFileBefore !== undefined
805
+ && after.mtimeMs <= portFileBefore.mtimeMs
806
+ && contents === portFileBefore.contents) return undefined
807
+ const value = Number(contents.split('\n')[0]?.trim() ?? '')
776
808
  return Number.isInteger(value) && value > 0 ? value : undefined
777
809
  } catch {
778
810
  return undefined