From 8a0bb9d9f4168c8003b534a1567ffe62ba359336 Mon Sep 17 00:00:00 2001 From: Sergey Rubanov Date: Fri, 29 Sep 2023 15:40:39 +0200 Subject: [PATCH 001/256] chore(npm): remove CONTRIBUTING.md from the npm package --- bin/publish-npm-modules.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/publish-npm-modules.sh b/bin/publish-npm-modules.sh index f364752da5..1862b702dd 100755 --- a/bin/publish-npm-modules.sh +++ b/bin/publish-npm-modules.sh @@ -148,7 +148,6 @@ if (( !only_platforms || only_top_level )); then cp -rf "$root/npm/bin/ssc.js" "$SOCKET_HOME/packages/$package/bin/ssc.js" cp -f "$root/LICENSE.txt" "$SOCKET_HOME/packages/$package" cp -f "$root/README.md" "$SOCKET_HOME/packages/$package/README-RUNTIME.md" - cp -f "$root/CONTRIBUTING.md" "$SOCKET_HOME/packages/$package/CONTRIBUTING.md" cp -rf "$root/api"/* "$SOCKET_HOME/packages/$package" fi From 75d92efcafe827ca0e80579b7203c0373b696da3 Mon Sep 17 00:00:00 2001 From: heapwolf Date: Mon, 2 Oct 2023 15:36:31 +0200 Subject: [PATCH 002/256] SRP rc for socket 0.5 --- api/stream-relay/cache.js | 2 +- api/stream-relay/encryption.js | 18 +- api/stream-relay/index.js | 551 ++++++++++++++++++++++----------- api/stream-relay/nat.js | 4 +- api/stream-relay/packets.js | 33 +- api/stream-relay/sugar.js | 463 +++++++++++++-------------- 6 files changed, 640 insertions(+), 431 deletions(-) diff --git a/api/stream-relay/cache.js b/api/stream-relay/cache.js index d59513ecb9..937685101e 100644 --- a/api/stream-relay/cache.js +++ b/api/stream-relay/cache.js @@ -220,7 +220,7 @@ export class Cache { if (!key || !key.slice) continue if (prefix.length && !key.startsWith(prefix)) continue const hex = key.slice(prefix.length, prefix.length + 1) - children[parseInt(hex, 16)].push(key) + if (children[parseInt(hex, 16)]) children[parseInt(hex, 16)].push(key) } // compute a checksum for all child members (deterministically) diff --git a/api/stream-relay/encryption.js b/api/stream-relay/encryption.js index 009c5fcba2..0430c63327 100644 --- a/api/stream-relay/encryption.js +++ b/api/stream-relay/encryption.js @@ -13,14 +13,14 @@ export class Encryption { return sodium.randombytes_buf(32) } - static async createClusterId (sharedKey) { - const key = sharedKey || await Encryption.createSharedKey() - return Buffer.from(key || '').toString('base64') - } - static async createKeyPair (seed) { await sodium.ready - seed = seed || await Encryption.createSharedKey() + seed = seed || sodium.randombytes_buf(32) + + if (typeof seed === 'string') { + seed = sodium.crypto_generichash(32, seed) + } + return sodium.crypto_sign_seed_keypair(seed) } @@ -28,6 +28,12 @@ export class Encryption { return (await sha256(str)).toString('hex') } + static async createClusterId (value) { + await sodium.ready + value = value || sodium.randombytes_buf(32) + return Buffer.from(value).toString('base64') + } + static async createSubclusterId (value) { return Buffer.from(value).toString('base64') } diff --git a/api/stream-relay/index.js b/api/stream-relay/index.js index e82b2ab517..00985ad954 100644 --- a/api/stream-relay/index.js +++ b/api/stream-relay/index.js @@ -28,7 +28,6 @@ import { PacketSync, PacketJoin, PacketQuery, - addHops, sha256, VERSION } from './packets.js' @@ -59,7 +58,7 @@ export const PROBE_WAIT = 512 * Default keep alive timeout. * @type {number} */ -export const DEFAULT_KEEP_ALIVE = 28_000 +export const DEFAULT_KEEP_ALIVE = 30_000 /** * Default rate limit threshold in milliseconds. @@ -103,10 +102,12 @@ const isReplicatable = type => ( export function rateLimit (rates, type, port, address, subclusterIdQuota) { const R = isReplicatable(type) const key = (R ? 'R' : 'C') + ':' + address + ':' + port - const quota = subclusterIdQuota || R ? 64 : 1024 - const time = Math.floor(Date.now() / 1000) + const quota = subclusterIdQuota || R ? 512 : 4096 + const time = Math.floor(Date.now() / 60000) const rate = rates.get(key) || { time, quota, used: 0 } + rate.mtime = Date.now() // checked by mainLoop for garabge collection + if (time !== rate.time) { rate.time = time if (rate.used > rate.quota) rate.quota -= 1 @@ -188,9 +189,13 @@ export class RemotePeer { const packets = await this.localPeer._message2packets(PacketStream, args.message, args) + if (this.proxy) { + debug(this.localPeer.peerId, `>> WRITE STREAM HAS PROXY ${this.proxy.address}:${this.proxy.port}`) + } + for (const packet of packets) { - debug(args.streamFrom, '>> WRITE STREAM', args.streamFrom, '->', args.streamTo, packet) - this.localPeer.send(await Packet.encode(packet), rinfo.port, rinfo.address) + debug(args.streamFrom, `>> WRITE STREAM (from=${args.streamFrom.slice(0, 6)}, to=${args.streamTo.slice(0, 6)}, via=${rinfo.address}:${rinfo.port})`) + this.localPeer.send(await Packet.encode(packet), rinfo.port, rinfo.address, this.socket) } } } @@ -214,7 +219,9 @@ export const wrap = dgram => { isListening = false ctime = Date.now() lastUpdate = 0 + lastSync = Date.now() closing = false + clock = 0 unpublished = {} cache = null uptime = 0 @@ -229,6 +236,8 @@ export const wrap = dgram => { streamBuffer = new Map() controlPackets = new Map() + metrics = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0 } + peers = JSON.parse(/* snapshot_start=1691579150299, filter=easy,static */`[ {"address":"3.70.160.181","port":41141,"peerId":"707c07171ac9371b2f1de23e78dad15d29b56d47abed5e5a187944ed55fc8483"}, {"address":"3.122.250.236","port":64236,"peerId":"a830615090d5cdc3698559764e853965a0d27baad0e3757568e6c7362bc6a12a"}, @@ -273,7 +282,6 @@ export const wrap = dgram => { this.config = { keepalive: DEFAULT_KEEP_ALIVE, lastGroupBroadcast: -1, - clock: 0, ...config } @@ -423,13 +431,15 @@ export const wrap = dgram => { for (const [k, packet] of [...this.cache.data]) { const p = Packet.from(packet) if (!p) continue + if (!p.timestamp) p.timestamp = ts - const ttl = Math.min(p.ttl, Packet.ttl) - const deadline = (p.timestamp ?? 0) + ttl + const ttl = (p.ttl < Packet.ttl) ? p.ttl : Packet.ttl + const deadline = p.timestamp + ttl if (deadline <= ts) { - this.mcast(p, p.packetId, true) + this.mcast(p) this.cache.delete(k) + debug(this.peerId, '-- DELETE', k, this.cache.size) if (this.onDelete) this.onDelete(p) } } @@ -454,8 +464,7 @@ export const wrap = dgram => { this.ping(peer, false, { requesterPeerId: this.peerId, natType: this.natType, - cacheSize: this.cache.size, - isHeartbeat: Date.now() + cacheSize: this.cache.size }) } @@ -466,6 +475,7 @@ export const wrap = dgram => { this.join(subcluster.sharedKey, subcluster) } } + return true } /** @@ -482,7 +492,7 @@ export const wrap = dgram => { if (err) return this._onError(err) const packet = Packet.decode(data) if (this.onSend) this.onSend(packet, port, address) - debug(`${this.peerId} -> SEND (T=${packet.type})`, port, address) + debug(this.peerId, `>> SEND (from=${this.address}:${this.port}, to=${address}:${port}, type=${packet.type})`) }) } @@ -494,7 +504,7 @@ export const wrap = dgram => { async sendUnpublished () { for (const [packetId] of Object.entries(this.unpublished)) { const packet = this.cache.get(packetId) - if (packet) this.mcast(packet, packetId) + if (packet) this.mcast(packet) } } @@ -515,11 +525,13 @@ export const wrap = dgram => { * @return {Array} * @ignore */ - getPeers (packet, peers, n = 3, filter = o => o) { + getPeers (packet, peers, ignorelist, filter = o => o) { + const n = 3 const rand = () => Math.random() - 0.5 const base = p => { - if (p.lastUpdate === 0) return false + if (ignorelist.findIndex(ilp => ilp.peerId === p.peerId) > -1) return false + if (p.lastUpdate !== 0 && p.lastUpdate > this.keepalive) return false if (this.peerId === p.peerId) return false // same as introducer if (packet.message.requesterPeerId === p.peerId) return false // same as requester if (!p.port || (!p.indexed && !NAT.isValid(p.natType))) return false @@ -527,21 +539,21 @@ export const wrap = dgram => { } const candidates = peers - .filter(base) .filter(filter) + .filter(base) .sort(rand) const list = candidates.filter(peer => peer.clusters && peer.clusters[packet.clusterId]) - if (!list.length) { - list.push(...candidates.filter(p => !p.indexed).slice(0, n)) + if (list.length < n) { + list.push(...candidates.filter(p => !p.indexed).slice(0, n - list.length)) } - if (!list.length) { - list.push(...candidates.filter(p => p.indexed).slice(0, n)) + if (list.length < n) { + list.push(...candidates.filter(p => p.indexed).slice(0, n - list.length)) } - return list.slice(0, 1).concat(candidates.sort(rand).slice(0, 2)) + return list } /** @@ -549,23 +561,13 @@ export const wrap = dgram => { * @return {undefined} * @ignore */ - async mcast (packet, packetId, isTaxed, ignorelist = []) { - const list = this.peers - - /* if (Array.isArray(packet.message?.history)) { - // TODO dedupe and trim - ignorelist = [...ignorelist, packet.message.history] - } + async mcast (packet, isTaxed, ignorelist = []) { + const peers = this.getPeers(packet, this.peers, ignorelist) - if (ignorelist.length) { - list = list.filter(p => !ignorelist.find(peer => { - return p.address === peer.address && p.port === peer.port - })) - } */ + packet.hops += 1 - for (const peer of this.getPeers(packet, list)) { - const data = await Packet.encode(packet) - this.send(isTaxed ? addHops(data) : data, peer.port, peer.address) + for (const peer of peers) { + this.send(await Packet.encode(packet), peer.port, peer.address) } if (this.controlPackets.has(packet.packetId)) return @@ -579,9 +581,14 @@ export const wrap = dgram => { */ async requestReflection () { if (this.onConnecting) this.onConnecting({ code: -2 }) - if (this.closing || this.indexed || this.reflectionId) return + + if (this.closing || this.indexed || this.reflectionId) { + debug(this.peerId, '<> REFLECT ABORTED', this.reflectionId) + return + } + + debug(this.peerId, '-> REQ REFLECT', this.reflectionId, this.reflectionStage) if (this.onConnecting) this.onConnecting({ code: -1 }) - debug(this.peerId, '-- NAT REQ REF') const peers = [...this.peers] .filter(p => p.lastUpdate !== 0) @@ -589,7 +596,7 @@ export const wrap = dgram => { if (peers.length < 2) { if (this.onConnecting) this.onConnecting({ code: 0 }) - debug(this.peerId, 'XX NOT ENOUGH PINGABLE PEERS - RETRYING') + debug(this.peerId, 'XX REFLECT NOT ENOUGH PINGABLE PEERS - RETRYING') return this._setTimeout(() => this.requestReflection(), 256) } @@ -618,8 +625,9 @@ export const wrap = dgram => { // we expect onMessageProbe to fire and clear this timer or it will timeout this.probeReflectionTimeout = this._setTimeout(() => { + this.probeReflectionTimeout = null if (this.reflectionStage !== 1) return - debug(this.peerId, 'XX NAT REFLECT - STAGE1: C - TIMEOUT', this.reflectionId, this.reflectionTimeout) + debug(this.peerId, 'XX NAT REFLECT - STAGE1: C - TIMEOUT', this.reflectionId) this.reflectionStage = 1 this.reflectionId = null @@ -646,7 +654,7 @@ export const wrap = dgram => { const peer2 = list.sort(() => Math.random() - 0.5)[0] if (!peer2) { // how did it advance? - debug(this.peerId, 'XX NAT REFLECT - STAGE2: NO PEERS HAVE BEEN PROBED YET - RETRYING', this.peers) + debug(this.peerId, 'XX NAT REFLECT - STAGE2: NO PEERS HAVE BEEN PROBED YET - RETRYING') return this._setTimeout(() => this.requestReflection(), 256) } @@ -661,12 +669,15 @@ export const wrap = dgram => { this.ping(peer1, false, { ...opts, probeExternalPort }) this.ping(peer2, false, { ...opts, probeExternalPort }) - if (this.reflectionTimeout) this._clearTimeout(this.reflectionTimeout) + if (this.reflectionTimeout) { + this._clearTimeout(this.reflectionTimeout) + this.reflectionTimeout = null + } this.reflectionTimeout = this._setTimeout(ts => { + this.reflectionTimeout = null if (this.reflectionStage !== 2) return debug(this.peerId, 'XX NAT REFLECT - STAGE2: TIMEOUT', this.reflectionId) - this.reflectionStage = 1 return this.requestReflection() }, 2048) } @@ -723,11 +734,7 @@ export const wrap = dgram => { */ addPeer (args) { const existingPeer = this.getPeer(args.peerId) - - if (existingPeer) { - existingPeer.lastUpdate = Date.now() - return existingPeer - } + if (existingPeer) return this.updatePeer(existingPeer) if (this.peers.length > 1024) { this.peers @@ -739,7 +746,10 @@ export const wrap = dgram => { if (!args.peerId) return null const peer = new RemotePeer(args, this) - peer.lastUpdate = Date.now() + if (args.connected) peer.lastUpdate = Date.now() + + if (args.clusterId) peer.clusters[args.clusterId] ??= {} + if (args.subclusterId) peer.clusters[args.subclusterId] = MAX_BANDWIDTH this.peers.push(peer) return peer } @@ -760,9 +770,9 @@ export const wrap = dgram => { const existingPeer = this.getPeer(args.peerId) if (!existingPeer) return this.addPeer(args) - existingPeer.lastUpdate = Date.now() + if (args.connected) existingPeer.lastUpdate = Date.now() - if (args.clock && existingPeer.clock > args.clock) { + if (isNaN(args.clock) || existingPeer.clock > args.clock) { return existingPeer } @@ -770,9 +780,18 @@ export const wrap = dgram => { if (args.pingId) existingPeer.pingId = args.pingId - existingPeer.clusters[args.clusterId] ??= {} + if (args.clusterId) existingPeer.clusters[args.clusterId] ??= {} if (args.subclusterId) existingPeer.clusters[args.subclusterId] = MAX_BANDWIDTH + debug(this.peerId, `<- PEER UPDATE (peerId=${args.peerId.slice(0, 6)}, clock=${args.clock}, address=${args.address}:${args.port})`) + + args.connected ||= existingPeer.connected + + if (!existingPeer.port) { + delete args.port + delete args.address + } + Object.assign(existingPeer, args) return existingPeer } @@ -781,26 +800,28 @@ export const wrap = dgram => { * This should be called at least once when an app starts to multicast * this peer, and starts querying the network to discover peers. * @param {object} keys - Created by `Encryption.createKeyPair()`. - * @param {object=} opts - Options - * @param {number=MAX_BANDWIDTH} opts.rateLimit - How many requests per second to allow for this subclusterId. + * @param {object=} args - Options + * @param {number=MAX_BANDWIDTH} args.rateLimit - How many requests per second to allow for this subclusterId. * @return {RemotePeer} */ - async join (sharedKey, opts = { rateLimit: MAX_BANDWIDTH }) { + async join (sharedKey, args = { rateLimit: MAX_BANDWIDTH }) { const keys = await Encryption.createKeyPair(sharedKey) this.encryption.add(keys.publicKey, keys.privateKey) if (!this.port || !this.natType || this.indexed) return - opts.sharedKey = sharedKey + args.sharedKey = sharedKey - const clusterId = opts.clusterId || this.config.clusterId + const clusterId = args.clusterId || this.config.clusterId const subclusterId = Buffer.from(keys.publicKey).toString('base64') this.clusters[clusterId] ??= {} - this.clusters[clusterId][subclusterId] = opts + this.clusters[clusterId][subclusterId] = args + + this.clock += 1 const packet = new PacketJoin({ - clock: this.config.clock++, + clock: this.clock, clusterId, subclusterId, message: { @@ -811,10 +832,9 @@ export const wrap = dgram => { } }) - debug(this.peerId, '-> JOIN', this.clusters) - + debug(this.peerId, `-> JOIN (clusterId=${clusterId}, clock=${packet.clock}/${this.clock})`) if (this.onState) await this.onState(this.getState()) - this.mcast(packet, packet.packetId, true) + this.mcast(packet) this.controlPackets.set(packet.packetId, 1) } @@ -829,8 +849,7 @@ export const wrap = dgram => { let messages = [message] const len = message?.byteLength ?? message?.length ?? 0 - - let clock = packet ? packet.clock : 0 + let clock = packet?.clock || 0 const siblings = [...this.cache.data.values()] .filter(Boolean) @@ -844,6 +863,8 @@ export const wrap = dgram => { clock = Math.max(clock, sib.clock) + 1 } + clock += 1 + if (len > 1024) { // Split packets that have messages bigger than Packet.maxLength messages = [{ meta, @@ -860,7 +881,7 @@ export const wrap = dgram => { subclusterId, streamTo: args.streamTo, streamFrom: args.streamFrom, - clock: ++clock, + clock, message, usr1, usr2, @@ -923,14 +944,12 @@ export const wrap = dgram => { const p = Packet.from(packet) this.cache.insert(packet.packetId, p) - if (this.onPacket) { - this.onPacket(p, this.port, this.address) - } + if (this.onPacket) this.onPacket(p, this.port, this.address, true) this.unpublished[packet.packetId] = Date.now() if (globalThis.navigator && !globalThis.navigator.onLine) continue - this.mcast(packet, packet.packetId) + this.mcast(packet) } return packets @@ -972,11 +991,13 @@ export const wrap = dgram => { * @ignore */ async _onConnection (packet, peer, port, address) { - if (this.closing || this.indexed) return - debug(this.peerId, '<- CONNECTION', peer.peerId, packet.type) + if (this.closing) return + + debug(this.peerId, '<- CONNECTION', peer.peerId, address, port, packet.type) if (!peer.localPeer) peer.localPeer = this this.sync(peer, packet, port, address) + if (this.onConnection) this.onConnection(packet, peer, port, address) } @@ -986,14 +1007,19 @@ export const wrap = dgram => { * @ignore */ async _onSync (packet, port, address) { + this.metrics[packet.type]++ + if (!isBufferLike(packet.message)) return - debug(this.peerId, '<< SYNC', port, address) + debug(this.peerId, '<< SYNC CACHE', port, address, this.cache.size) const remote = Cache.decodeSummary(packet.message) const local = await this.cache.summarize(remote.prefix) - if (local.hash === remote.hash) return // caches are sync'd + if (local.hash === remote.hash) { + if (this.onSyncFinished) this.onSyncFinished() + return + } for (let i = 0; i < local.buckets.length; i++) { // @@ -1017,7 +1043,7 @@ export const wrap = dgram => { if (this.onWarn) this.onWarn(err) continue } - this.send(data, port, address) + this._setTimeout(() => this.send(data, port, address), 2048) } } else if (remote.buckets[i] !== local.buckets[i]) { // @@ -1028,7 +1054,7 @@ export const wrap = dgram => { const data = await Packet.encode(new PacketSync({ message: encoded })) - this.send(data, port, address) + this._setTimeout(() => this.send(data, port, address), 2048) } } } @@ -1039,12 +1065,12 @@ export const wrap = dgram => { * @ignore */ async _onQuery (packet, port, address) { + this.metrics[packet.type]++ + debug(this.peerId, '<- QUERY', port, address) if (this.controlPackets.has(packet.packetId)) return this.controlPackets.set(packet.packetId, 1) - const { packetId } = packet - if (this.onEvaluateQuery) await this.onEvaluateQuery(packet, port, address) if (!Array.isArray(packet.message.history)) { @@ -1054,7 +1080,7 @@ export const wrap = dgram => { packet.message.history.push({ address: this.address, port: this.port }) if (packet.message.history.length >= 16) packet.message.history.shift() - return await this.mcast(packet, packetId, true) + return await this.mcast(packet) } /** @@ -1063,12 +1089,14 @@ export const wrap = dgram => { * @ignore */ async _onPing (packet, port, address) { + this.metrics[packet.type]++ + this.lastUpdate = Date.now() + debug(this.peerId, `<- PING (from=${address}:${port})`) const { reflectionId, isReflection, isConnection, isHeartbeat } = packet.message if (packet.message.requesterPeerId === this.peerId) return - debug(this.peerId, '<- PING', port, address) - const { probeExternalPort, isProbe } = packet.message + const { probeExternalPort, isProbe, pingId } = packet.message let peer = this.getPeer(packet.message.requesterPeerId) @@ -1080,7 +1108,6 @@ export const wrap = dgram => { const peerId = packet.message.requesterPeerId const natType = packet.message.natType peer = this.addPeer({ peerId, natType, port, address }) - if (!peer) return } if (isHeartbeat) return @@ -1101,6 +1128,10 @@ export const wrap = dgram => { if (reflectionId) message.reflectionId = reflectionId if (isHeartbeat) message.isHeartbeat = Date.now() + if (pingId) { + message.pingId = pingId + message.isDebug = true + } if (isReflection) { message.isReflection = true @@ -1136,18 +1167,21 @@ export const wrap = dgram => { * @ignore */ async _onPong (packet, port, address) { + this.metrics[packet.type]++ + this.lastUpdate = Date.now() const { reflectionId, pingId, isReflection, responderPeerId } = packet.message - debug(this.peerId, '<- PONG', port, address, reflectionId) + + debug(this.peerId, '<- PONG', port, address) + const peer = this.updatePeer({ peerId: packet.message.responderPeerId, port, address }) + if (!peer) return + + peer.lastUpdate = Date.now() if (packet.message.isConnection) { - const peer = this.getPeer(responderPeerId) - if (!peer) return if (pingId) peer.pingId = pingId - peer.lastUpdate = Date.now() - this._onConnection(packet, peer, packet.message.port, packet.message.address) return } @@ -1190,14 +1224,19 @@ export const wrap = dgram => { if (NAT.isValid(natType)) { const oldType = this.natType this.natType = natType + this.reflectionId = null + this.reflectionStage = 0 if (natType !== oldType) { // alert both peers of our new NAT type - [ + const peersToUpdate = [ this.getPeer(responderPeerId), this.getPeer(this.reflectionFirstResponder.responderPeerId) - ].forEach(peer => { + ] + + peersToUpdate.forEach(peer => { if (!peer) return + peer.lastRequest = Date.now() this.ping(peer, false, { requesterPeerId: this.peerId, @@ -1225,9 +1264,6 @@ export const wrap = dgram => { this.port = packet.message.port debug(this.peerId, `++ NAT UPDATE STATE (address=${this.address}, port=${this.port})`) } - - const peer = this.getPeer(responderPeerId) - if (peer) peer.lastUpdate = Date.now() } /** @@ -1236,11 +1272,20 @@ export const wrap = dgram => { * @ignore */ async _onIntro (packet, port, address) { + this.metrics[packet.type]++ + if (packet.message.timestamp + this.config.keepalive < Date.now()) return if (packet.message.requesterPeerId === this.peerId) return // intro to myself? if (packet.message.responderPeerId === this.peerId) return // intro from myself? if (packet.hops > this.maxHops) return + debug(this.peerId, '<- INTRO (' + + `isRendezvous=${packet.message.isRendezvous}, ` + + `from=${address}:${port}, ` + + `to=${packet.message.address}:${packet.message.port}, ` + + `clustering=${packet.clusterId.slice(0, 4)}/${packet.subclusterId.slice(0, 4)}` + + ')') + const { clusterId, subclusterId, clock } = packet // this is the peer that is being introduced to the new peers @@ -1248,12 +1293,17 @@ export const wrap = dgram => { const peerPort = packet.message.port const peerAddress = packet.message.address const natType = packet.message.natType - const peer = this.updatePeer({ peerId, natType, port: peerPort, address: peerAddress, clock, clusterId, subclusterId }) + // update the info about the peer that is introducing us + // this.updatePeer({ peerId: packet.message.responderPeerId, port, address }) + + // update the info about the peer we are being introduced to - if (peer.connected || peer.connecting) return // already connecting + const peer = this.updatePeer({ peerId, natType, port: peerPort, address: peerAddress, clock, clusterId, subclusterId }) + if (!peer) return // not enough information provided to create a peer + if (peer.connecting) return // already connecting const pingId = Math.random().toString(16).slice(2) - const { hash } = await this.cache.summarize() + const { hash } = await this.cache.summarize('') const props = { natType: this.natType, @@ -1262,29 +1312,26 @@ export const wrap = dgram => { requesterPeerId: this.peerId } - const makePacket = async () => await Packet.encode(new PacketPing({ - message: { - requesterPeerId: this.peerId, - cacheSummaryHash: hash || null, - natType: this.natType, - uptime: this.uptime, - isConnection: true, - timestamp: Date.now(), - pingId - } - })) - - let strategy = NAT.connectionStrategy(this.natType, packet.message.natType) + const strategy = NAT.connectionStrategy(this.natType, packet.message.natType) - debug(this.peerId, `++ NAT STRATEGY=${NAT.toStringStrategy(strategy)} (from=${this.address}/${NAT.toString(this.natType)}, to=${packet.message.address}/${NAT.toString(packet.message.natType)})`) + debug(this.peerId, `++ NAT INTRO (strategy=${NAT.toStringStrategy(strategy)}, from=${this.address}:${this.port} [${NAT.toString(this.natType)}], to=${packet.message.address}:${packet.message.port} [${NAT.toString(packet.message.natType)}])`) if (strategy === NAT.STRATEGY_TRAVERSAL_CONNECT) { - this.send(await makePacket(), port, packet.message.address) + if (peer.connecting) return + + debug(this.peerId, `## NAT CONNECT (from=${this.address}:${this.port}, to=${peerAddress}:${peerPort}, pingId=${pingId})`) + + this.ping(peer, true) + const portCache = new Set() peer.connecting = true let i = 0 + if (!this.socketPool) { + this.socketPool = Array.from({ length: 1024 }, () => dgram.createSocket('udp4', null, this)) + } + // A probes 1 target port on B from 1024 source ports // (this is 1.59% of the search clusterId) // B probes 256 target ports on A from 1 source port @@ -1292,16 +1339,15 @@ export const wrap = dgram => { // // Probability of successful traversal: 98.35% // - const interval = this._setInterval(() => { + const interval = setInterval(async () => { // TODO should be this._setInterval // send messages until we receive a message from them. giveup after sending ยฑ1024 // packets and fall back to using the peer that sent this as the initial proxy. - if (i++ > 1024) { + if (i++ >= 1024) { this._clearInterval(interval) + clearInterval(interval) peer.connecting = false - strategy = NAT.STRATEGY_PROXY - peer.proxy = this.peers.find(p => p.address === address && p.port === port) - this._onConnection(packet, peer, port, address) + this._setTimeout(() => this._onIntro(packet, port, address), 2048) return false } @@ -1314,51 +1360,120 @@ export const wrap = dgram => { peer.connecting = false pair.connected = true this._onConnection(packet, pair, pair.port, pair.address) + + if (this.onJoin && this.clusters[packet.clusterId]) { + this.onJoin(packet, peer, peerPort, peerAddress) + } + this._clearInterval(interval) + clearInterval(interval) return false } - const to = i === 0 ? packet.message.port : getRandomPort(portCache) - ;(async () => this.send(await makePacket(), to, packet.message.address))() + const to = getRandomPort(portCache) + const data = await Packet.encode(new PacketPing({ + message: { + requesterPeerId: this.peerId, + cacheSummaryHash: hash || null, + natType: this.natType, + uptime: this.uptime, + isConnection: true, + timestamp: Date.now(), + pingId + } + })) + + const rand = () => Math.random() - 0.5 + const pooledSocket = this.socketPool.sort(rand).find(s => !s.socket) + if (!pooledSocket) return // TODO recover from exausted socket pool + + try { + pooledSocket.send(data, to, packet.message.address) + } catch (err) { + } + + pooledSocket.on('message', (...args) => { + clearTimeout(pooledSocket.expire) + const remote = this.peers.find(r => r.peerId === peerId) + remote.socket = pooledSocket + + clearInterval(interval) + this._onMessage(...args) + }) + + if (!pooledSocket.socket) { + pooledSocket.expire = setTimeout(() => { + if (pooledSocket._bindState === 2) { + try { + pooledSocket.close() + } catch (err) {} + } + this.socketPool[i] = dgram.createSocket('udp4', null, this) + }, 2048) + } }, 10) return } - if (strategy === NAT.STRATEGY_PROXY) { - peer.proxy = this.peers.find(p => p.address === address && p.port === port) + if (strategy === NAT.STRATEGY_PROXY && !peer.proxy) { + // TODO could allow multiple proxies + let proxy = this.peers.find(p => p.address === address && p.port === port) + + if (!proxy) { + proxy = this.addPeer({ peerId: packet.message.responderPeerId, port, address, natType: NAT.UNRESTRICTED }) + debug(this.peerId, '++ INTRO CRAETED PROXY', packet.message.responderPeerId) + } + + if (proxy) { + peer.proxy = proxy + debug(this.peerId, '++ INTRO CHOSE PROXY STRATEGY', peer.proxy.address) + } } if (strategy === NAT.STRATEGY_TRAVERSAL_OPEN) { + if (peer.opening) return + peer.opening = true + if (!this.bdpCache.length) { - this.bdpCache = Array.from({ length: 256 }, () => getRandomPort()) + globalThis.bdpCache = this.bdpCache = Array.from({ length: 256 }, () => getRandomPort()) } this.ping(peer, true, props) for (const port of this.bdpCache) { - this.send(await makePacket(), port, packet.message.address) + const data = await Packet.encode(new PacketPing({ + message: { + requesterPeerId: this.peerId, + cacheSummaryHash: hash || null, + natType: this.natType, + uptime: this.uptime, + isConnection: true, + timestamp: Date.now() + } + })) + this.send(data, port, packet.message.address) } return } - if (strategy === NAT.STRATEGY_PROXY) { - debug(this.peerId, '++ SET PROXY', peer.peerId) - } - if (strategy === NAT.STRATEGY_DIRECT_CONNECT) { - peer.connected = true + peer.connected = true // TODO this could be req/res debug(this.peerId, '++ NAT STRATEGY_DIRECT_CONNECT') } if (strategy === NAT.STRATEGY_DEFER) { + peer.connected = true // TODO this could be req/res debug(this.peerId, '++ NAT STRATEGY_DEFER') - peer.connected = true } this.ping(peer, true, props) - this._onConnection(packet, peer, port, address) + if (packet.hops === 1) this._onConnection(packet, peer, port, address) + + if (this.onJoin && this.clusters[packet.clusterId]) { + this.onJoin(packet, peer, port, address) + } } /** @@ -1367,10 +1482,10 @@ export const wrap = dgram => { * @ignore */ async _onJoin (packet, port, address, data) { - debug(this.peerId, '<- JOIN', packet.hops, port, address) + this.metrics[packet.type]++ + if (packet.message.requesterPeerId === this.peerId) return if (!packet.clusterId) return - if (packet.hops > this.maxHops) return this.lastUpdate = Date.now() @@ -1378,22 +1493,47 @@ export const wrap = dgram => { const natType = packet.message.natType const subclusterId = packet.subclusterId const clusterId = packet.clusterId + const clock = packet.clock + const peerAddress = packet.message.address + const peerPort = packet.message.port - const peer = this.updatePeer({ peerId, natType, port, address, subclusterId, clusterId }) - this.sync(peer, packet, port, address) - - let peers = this.getPeers(packet, this.peers) + debug(this.peerId, '<- JOIN (' + + `peerId=${peerId.slice(0, 6)}, ` + + `clock=${packet.clock}, ` + + `hops=${packet.hops}, ` + + `clusterId=${packet.clusterId}, ` + + `address=${address}:${port})` + ) + + this.updatePeer({ + peerId, + natType, + port: natType === NAT.ENDPOINT_RESTRICTED ? port : packet.message.port, + clock, + connected: packet.hops === 1, + address: peerAddress, + subclusterId, + clusterId + }) - if (packet.message.rendezvousDeadline) { - if (packet.message.rendezvousDeadline < Date.now()) return + const filter = p => p.connected || p.natType === NAT.UNRESTRICTED // you can't intro a peer who aren't connecteds + let peers = this.getPeers(packet, this.peers, [{ port, address }], filter) - if (this.clusters[packet.clusterId]) { + // + // This packet represents a peer who wants to join the network and is a + // member of our cluster. The packet was replicated though the network + // and contains the details about where the peer can be reached, in this + // case we want to ping that peer so we can be introduced to them. + // + if (packet.message.rendezvousDeadline && !packet.message.rendezvousRequesterPeerId) { + if (packet.message.rendezvousDeadline > Date.now() && this.clusters[packet.clusterId]) { // TODO it would tighten up the transition time between dropped peers // if we check strategy from (packet.message.natType, this.natType) and // make introductions that create more mutually known peers. + debug(this.peerId, '<- INTRO JOIN RENDEZVOUS INTERCEPTED') const data = await Packet.encode(new PacketJoin({ - clock: this.config.clock++, + clock: packet.clock, subclusterId: packet.subclusterId, clusterId: packet.clusterId, message: { @@ -1402,7 +1542,7 @@ export const wrap = dgram => { address: this.address, port: this.port, rendezvousType: packet.message.natType, - rendezvousPeerId: packet.message.peerId + rendezvousRequesterPeerId: packet.message.requesterPeerId } })) @@ -1411,16 +1551,26 @@ export const wrap = dgram => { packet.message.rendezvousPort, packet.message.rendezvousAddress ) - return } } - if (packet.message.rendezvousPeerId) { - debug(this.peerId, '<- RENDEZVOUS') - const peer = this.peers.find(p => p.peerId === packet.message.rendezvousPeerId) + // + // A peer who belongs to the same cluster as the peer who's replicated + // join was discovered, sent us a join that has a specification for who + // they want to be introduced to. + // + if (packet.message.rendezvousRequesterPeerId && this.peerId === packet.message.rendezvousPeerId) { + const peer = this.peers.find(p => p.peerId === packet.message.rendezvousRequesterPeerId) + + if (!peer) { + debug(this.peerId, '<- INTRO JOIN RENDEZVOUS FAILED', packet) + return + } + peer.natType = packet.message.rendezvousType - if (!peer) return peers = [peer] + + debug(this.peerId, '<- INTRO JOIN RENDEZVOUS') } for (const peer of peers) { @@ -1430,7 +1580,6 @@ export const wrap = dgram => { requesterPeerId: peer.peerId, responderPeerId: this.peerId, isRendezvous: !!packet.message.rendezvousPeerId, - timestamp: Date.now(), natType: peer.natType, address: peer.address, port: peer.port @@ -1440,43 +1589,54 @@ export const wrap = dgram => { requesterPeerId: packet.message.requesterPeerId, responderPeerId: this.peerId, isRendezvous: !!packet.message.rendezvousPeerId, - timestamp: Date.now(), natType: packet.message.natType, address: packet.message.address, port: packet.message.port } - const clock = packet.message?.clock || 0 + const opts1 = { + hops: packet.hops + 1 + } + + if (peer.clusters && peer.clusters[clusterId]) { + opts1.clusterId = clusterId + if (peer.clusters[clusterId][subclusterId]) opts1.subclusterId = subclusterId + } - const opts = { + const opts2 = { + hops: packet.hops + 1, clusterId, - subclusterId, - clock: clock + 1 + subclusterId } - const intro1 = await Packet.encode(new PacketIntro({ ...opts, message: message1 })) - const intro2 = await Packet.encode(new PacketIntro({ ...opts, message: message2 })) + const intro1 = await Packet.encode(new PacketIntro({ ...opts1, message: message1 })) + const intro2 = await Packet.encode(new PacketIntro({ ...opts2, message: message2 })) // // Send intro1 to the peer described in the message // Send intro2 to the peer in this loop // - debug(this.peerId, `>> INTRO ${peer.address}:${peer.port} -> ${packet.message.address}:${packet.message.port}`) - debug(this.peerId, `>> INTRO ${packet.message.address}:${packet.message.port} -> ${peer.address}:${peer.port}`) + debug(this.peerId, `>> INTRO SEND (from=${peer.address}:${peer.port}, to=${packet.message.address}:${packet.message.port})`) + debug(this.peerId, `>> INTRO SEND (from=${packet.message.address}:${packet.message.port}, to=${peer.address}:${peer.port})`) this.send(intro2, peer.port, peer.address) this.send(intro1, packet.message.port, packet.message.address) } if (this.indexed && !packet.clusterId) return - if (packet.message.rendezvousPeerId) return + if (packet.hops > this.maxHops) return - packet.message.rendezvousAddress = this.address - packet.message.rendezvousPort = this.port - packet.message.rendezvousDeadline = Date.now() + this.config.keepalive + if (this.natType === NAT.UNRESTRICTED && !packet.message.rendezvousDeadline) { + packet.message.rendezvousAddress = this.address + packet.message.rendezvousPort = this.port + packet.message.rendezvousType = this.natType + packet.message.rendezvousPeerId = this.peerId + packet.message.rendezvousDeadline = Date.now() + this.config.keepalive + } - if (packet.hops++ > this.maxHops) return - this.mcast(packet, packet.packetId, true) + debug(this.peerId, `-> JOIN RELAY (peerId=${peerId.slice(0, 6)}, from=${peerAddress}:${peerPort})`) + this.mcast(packet, [{ port, address }, { port: peerPort, address: peerAddress }]) + this.controlPackets.set(packet.packetId, 2) } /** @@ -1485,7 +1645,9 @@ export const wrap = dgram => { * @ignore */ async _onPublish (packet, port, address, data) { - debug(this.peerId, '<- PUBLISH', port, address) + this.metrics[packet.type]++ + + debug(this.peerId, '<- PUBLISH', packet.clusterId, packet.subclusterId, port, address) // const subclusterId = this.clusters[packet.clusterId] // only cache if this packet if i am part of this subclusterId @@ -1493,6 +1655,8 @@ export const wrap = dgram => { if (this.cache.has(packet.packetId)) return this.cache.insert(packet.packetId, packet) + const ignorelist = [{ address, port }] + if (!this.indexed && this.encryption.has(packet.subclusterId)) { let p = { ...packet } if (p.index > -1) p = await this.cache.compose(p) @@ -1501,8 +1665,9 @@ export const wrap = dgram => { if (this.onPacket && p.index === -1) this.onPacket(p, port, address) } else { // if we cant find the packet, sync with some more peers - for (const peer of this.getPeers(packet, this.peers)) { - this.sync(peer, packet, peer.port, peer.address) + for (const peer of this.getPeers(packet, this.peers, ignorelist)) { + // const peer = this.peers.find(p => p.address === address && p.port === port) + if (peer) this.sync(peer, packet, peer.port, peer.address) } } } @@ -1510,7 +1675,7 @@ export const wrap = dgram => { if (packet.hops > this.maxHops) return if (this.onMulticast) this.onMulticast(packet) - this.mcast(packet, packet.packetId, true, [{ address, port }]) + this.mcast(packet, ignorelist) } /** @@ -1519,43 +1684,59 @@ export const wrap = dgram => { * @ignore */ async _onStream (packet, port, address, data) { - debug(this.peerId, '<- ON STREAM', address, port) + this.metrics[packet.type]++ + const { streamTo, streamFrom } = packet + // only help packets with a higher hop count if they are in our cluster + // if (packet.hops > 2 && !this.clusters[packet.cluster]) return + const peerFrom = this.peers.find(p => p.peerId === streamFrom) if (!peerFrom) return // stream message is for this peer - if (streamTo === this.peerId && this.encryption.has(packet.subclusterId)) { - let p = Packet.from(packet) // clone the packet so it's not modified + if (streamTo === this.peerId) { + if (this.encryption.has(packet.subclusterId)) { + let p = Packet.from(packet) // clone the packet so it's not modified - if (p.index > -1) { // if it needs to be composed... - packet.timestamp = Date.now() - this.streamBuffer.set(packet.packetId, packet) // cache the partial + if (p.index > -1) { // if it needs to be composed... + packet.timestamp = Date.now() + this.streamBuffer.set(packet.packetId, packet) // cache the partial - p = await this.cache.compose(p, this.streamBuffer) // try to compose - if (!p) return // could not compose + p = await this.cache.compose(p, this.streamBuffer) // try to compose + if (!p) return // could not compose - if (p) { // if successful, delete the artifacts - const previousId = packet.index === 0 ? packet.packetId : packet.previousId + if (p) { // if successful, delete the artifacts + const previousId = packet.index === 0 ? packet.packetId : packet.previousId - this.streamBuffer.forEach((v, k) => { - if (k === previousId) this.streamBuffer.delete(k) - if (v.previousId === previousId) this.streamBuffer.delete(k) - }) + this.streamBuffer.forEach((v, k) => { + if (k === previousId) this.streamBuffer.delete(k) + if (v.previousId === previousId) this.streamBuffer.delete(k) + }) + } } + + if (this.onStream) this.onStream(p, peerFrom, port, address) } - if (this.onStream) this.onStream(p, peerFrom, port, address) return } // stream message is for another peer const peerTo = this.peers.find(p => p.peerId === streamTo) - if (!peerTo) return - if (packet.hops > this.maxHops) return + if (!peerTo) { + debug(this.peerId, `XX STREAM RELAY FORWARD DESTINATION NOT REACHABLE (to=${streamTo})`) + return + } + + if (packet.hops > this.maxHops) { + debug(this.peerId, `XX STREAM RELAY MAX HOPS EXCEEDED (to=${streamTo})`) + return + } - return this.send(addHops(data), peerTo.port, peerTo.address) + debug(this.peerId, `>> STREAM RELAY (to=${peerTo.address}:${peerTo.port}, id=${peerTo.peerId.slice(0, 6)})`) + this.send(await Packet.encode(packet), peerTo.port, peerTo.address) + if (packet.hops <= 2 && this.natType === NAT.UNRESTRICTED) this.mcast(packet) } /** @@ -1625,7 +1806,7 @@ export const wrap = dgram => { * @param {Buffer|Uint8Array} data * @param {{ port: number, address: string }} info */ - _onMessage (data, { port, address }) { + async _onMessage (data, { port, address }) { const packet = Packet.decode(data) if (!packet || packet.version < VERSION) return @@ -1646,7 +1827,7 @@ export const wrap = dgram => { if (!this.config.limitExempt) { if (rateLimit(this.rates, packet.type, port, address, subclusterId)) { - debug(this.peerId, 'XX RATE LIMITED', packet.type, packet.hops, packet.packetId, 'FROM', address) + debug(this.peerId, `XX RATE LIMIT HIT (from=${address}, type=${packet.type})`) return } if (this.onLimit && !this.onLimit(packet, port, address)) return @@ -1663,7 +1844,7 @@ export const wrap = dgram => { } const peer = this.peers.find(p => p.address === address && p.port === port) - if (peer) peer.lastUpdate = Date.now() + if (peer && packet.hops === 1) peer.lastUpdate = Date.now() if (!this.natType && !this.indexed) return diff --git a/api/stream-relay/nat.js b/api/stream-relay/nat.js index 327ecc377b..f577214641 100644 --- a/api/stream-relay/nat.js +++ b/api/stream-relay/nat.js @@ -165,7 +165,7 @@ export const connectionStrategy = (a, b) => { case UNRESTRICTED: return STRATEGY_DEFER case ADDR_RESTRICTED: return STRATEGY_DIRECT_CONNECT // a is guessing, b is hinting case PORT_RESTRICTED: return STRATEGY_DIRECT_CONNECT // both guess, will take too long, most resign to proxying - case ENDPOINT_RESTRICTED: return STRATEGY_TRAVERSAL_CONNECT // try connecting + case ENDPOINT_RESTRICTED: return STRATEGY_TRAVERSAL_OPEN // try connecting } break } @@ -175,7 +175,7 @@ export const connectionStrategy = (a, b) => { switch (a) { case UNRESTRICTED: return STRATEGY_DEFER case ADDR_RESTRICTED: return STRATEGY_DIRECT_CONNECT // the 3 successive packets will penetrate - case PORT_RESTRICTED: return STRATEGY_TRAVERSAL_OPEN // open up some ports + case PORT_RESTRICTED: return STRATEGY_TRAVERSAL_CONNECT // open up some ports case ENDPOINT_RESTRICTED: return STRATEGY_PROXY // unroutable } } diff --git a/api/stream-relay/packets.js b/api/stream-relay/packets.js index 86f5213d72..4532e20dad 100644 --- a/api/stream-relay/packets.js +++ b/api/stream-relay/packets.js @@ -83,7 +83,7 @@ export const MAGIC_BYTES_PREFIX = [0x03, 0x05, 0x0b, 0x11] /** * The version of the protocol. */ -export const VERSION = 3 +export const VERSION = 4 /** * The size in bytes of the prefix magic bytes. @@ -279,8 +279,8 @@ export const decode = buf => { o.packetId = trim(buf.slice(offset, offset += MESSAGE_ID_BYTES)).toString('hex') o.previousId = trim(buf.slice(offset, offset += PREVIOUS_ID_BYTES)).toString('hex') o.nextId = trim(buf.slice(offset, offset += NEXT_ID_BYTES)).toString('hex') - o.streamTo = trim(buf.slice(offset, offset += STREAM_TO_BYTES)).toString() - o.streamFrom = trim(buf.slice(offset, offset += STREAM_FROM_BYTES)).toString() + o.streamTo = trim(buf.slice(offset, offset += STREAM_TO_BYTES)).toString('hex') + o.streamFrom = trim(buf.slice(offset, offset += STREAM_FROM_BYTES)).toString('hex') // extract 32-byte public-key (if any) and encode as base64 string // @ts-ignore @@ -382,6 +382,7 @@ export class Packet { this.type = options?.type || 0 this.version = VERSION this.clock = options?.clock || 0 + this.hops = options?.hops || 0 this.index = typeof options?.index === 'undefined' ? -1 : options.index this.clusterId = options?.clusterId || '' this.subclusterId = options?.subclusterId || '' @@ -403,6 +404,8 @@ export class Packet { p = { ...p } const buf = Buffer.alloc(PACKET_BYTES) // buf length bust be < UDP MTU (usually ~1500) + if (!p.message) return buf + const isBuffer = isBufferLike(p.message) const isObject = typeof p.message === 'object' @@ -412,7 +415,7 @@ export class Packet { p.message = String(p.message) } - if (p.message.length > Packet.MESSAGE_BYTES) throw new Error('ETOOBIG') + if (p.message?.length > Packet.MESSAGE_BYTES) throw new Error('ETOOBIG') // we only have p.nextId when we know ahead of time, if it's empty that's fine. p.packetId = p.packetId || await sha256(p.previousId + p.message + p.nextId) @@ -435,8 +438,8 @@ export class Packet { Buffer.from(p.packetId, 'hex').copy(buf, offset); offset += MESSAGE_ID_BYTES Buffer.from(p.previousId, 'hex').copy(buf, offset); offset += PREVIOUS_ID_BYTES Buffer.from(p.nextId, 'hex').copy(buf, offset); offset += NEXT_ID_BYTES - Buffer.from(p.streamTo).copy(buf, offset); offset += STREAM_TO_BYTES - Buffer.from(p.streamFrom).copy(buf, offset); offset += STREAM_FROM_BYTES + Buffer.from(p.streamTo, 'hex').copy(buf, offset); offset += STREAM_TO_BYTES + Buffer.from(p.streamFrom, 'hex').copy(buf, offset); offset += STREAM_FROM_BYTES Buffer.from(p.clusterId, 'base64').copy(buf, offset); offset += CLUSTER_ID_BYTES Buffer.from(p.subclusterId, 'base64').copy(buf, offset); offset += SUBCLUSTER_ID_BYTES @@ -480,6 +483,7 @@ export class PacketPing extends Packet { isConnection: { type: 'boolean' }, isReflection: { type: 'boolean' }, isProbe: { type: 'boolean' }, + isDebug: { type: 'boolean' }, timestamp: { type: 'number' } }) } @@ -503,6 +507,8 @@ export class PacketPong extends Packet { isHeartbeat: { type: 'number' }, isConnection: { type: 'boolean' }, reflectionId: { type: 'string' }, + pingId: { type: 'string' }, + isDebug: { type: 'boolean' }, isProbe: { type: 'boolean' }, rejected: { type: 'boolean' } }) @@ -511,8 +517,8 @@ export class PacketPong extends Packet { export class PacketIntro extends Packet { static type = 3 - constructor ({ clock, message }) { - super({ type: PacketIntro.type, clock, message }) + constructor ({ clock, hops, clusterId, subclusterId, message }) { + super({ type: PacketIntro.type, clock, hops, clusterId, subclusterId, message }) validatePacket(message, { subclusterId: { type: 'string' }, @@ -529,15 +535,16 @@ export class PacketIntro extends Packet { export class PacketJoin extends Packet { static type = 4 - constructor ({ clock, clusterId, message }) { - super({ type: PacketJoin.type, clock, clusterId, message }) + constructor ({ clock, hops, clusterId, subclusterId, message }) { + super({ type: PacketJoin.type, clock, hops, clusterId, subclusterId, message }) validatePacket(message, { - rendezvousDeadline: { type: 'number' }, rendezvousAddress: { type: 'string' }, rendezvousPort: { type: 'number' }, rendezvousType: { type: 'number' }, rendezvousPeerId: { type: 'string' }, + rendezvousDeadline: { type: 'number' }, + rendezvousRequesterPeerId: { type: 'string' }, subclusterId: { type: 'string' }, requesterPeerId: { required: true, type: 'string' }, natType: { required: true, type: 'number' }, @@ -550,8 +557,8 @@ export class PacketJoin extends Packet { export class PacketPublish extends Packet { static type = 5 // no need to validatePacket, message is whatever you want - constructor ({ message, sig, packetId, clusterId, subclusterId, nextId, clock, to, usr1, usr2, ttl, previousId }) { - super({ type: PacketPublish.type, message, sig, packetId, clusterId, subclusterId, nextId, clock, usr1, usr2, ttl, previousId }) + constructor ({ message, sig, packetId, clusterId, subclusterId, nextId, clock, hops, to, usr1, usr2, ttl, previousId }) { + super({ type: PacketPublish.type, message, sig, packetId, clusterId, subclusterId, nextId, clock, hops, usr1, usr2, ttl, previousId }) } } diff --git a/api/stream-relay/sugar.js b/api/stream-relay/sugar.js index 822d8269cc..8dbe845785 100644 --- a/api/stream-relay/sugar.js +++ b/api/stream-relay/sugar.js @@ -2,10 +2,10 @@ import { wrap, Encryption, sha256, NAT } from './index.js' import { sodium } from '../crypto.js' import { Buffer } from '../buffer.js' import { isBufferLike } from '../util.js' -import { PacketStream } from './packets.js' +import { CACHE_TTL, PACKET_BYTES } from './packets.js' export default (dgram, events) => { - let peer = null + let _peer = null let bus = null /* @@ -14,6 +14,9 @@ export default (dgram, events) => { * The method returned method should be exposed to the user of the module. * * @param {object} - options + * @param {clusterId} - options.clusterId + * @param {peerId} - options.peerId + * * @return {object} * * @example @@ -29,40 +32,163 @@ export default (dgram, events) => { await sodium.ready bus = new events.EventEmitter() + bus.peers = new Map() + bus._on = bus.on + bus._once = bus.once + bus._emit = bus.emit - if (!options.indexed && !options.clusterId) { + if (!options.indexed && !options.clusterId && !options.config?.clusterId) { throw new Error('expected .clusterId property') } - const clusterId = bus.clusterId = options.clusterId - - peer = new (wrap(dgram))(options) - - peer.onPacket = (...args) => setTimeout(() => bus.emit('#packet', ...args)) - peer.onData = (...args) => setTimeout(() => bus.emit('#data', ...args)) - peer.onSend = (...args) => setTimeout(() => bus.emit('#send', ...args)) - peer.onMulticast = (...args) => setTimeout(() => bus.emit('#multicast', ...args)) - peer.onStream = (...args) => setTimeout(() => bus.emit('#stream', ...args)) - peer.onConnection = (...args) => setTimeout(() => bus.emit('#connection', ...args)) - peer.onDisconnection = (...args) => bus.emit('#disconnection', ...args) - peer.onQuery = (...args) => setTimeout(() => bus.emit('#query', ...args)) - peer.onNat = (...args) => bus.emit('#network-change', ...args) - peer.onError = (...args) => bus.emit('#error', ...args) - peer.onWarn = (...args) => bus.emit('#warning', ...args) - peer.onState = (...args) => bus.emit('#state', ...args) - peer.onConnecting = (...args) => bus.emit('#connecting', ...args) - - peer.onReady = () => { - peer.isReady = true - bus.emit('#ready') + const clusterId = bus.clusterId = options.clusterId || options.config?.clusterId + + _peer = new (wrap(dgram))(options) // only one peer per process makes sense + + _peer.onPacket = (packet, ...args) => { + if (packet.clusterId !== clusterId) return + bus._emit('#packet', packet, ...args) + } + + _peer.onJoin = (packet, ...args) => { + if (packet.clusterId !== clusterId) return + bus._emit('#join', packet, ...args) + } + + _peer.onStream = (packet, ...args) => { + if (packet.clusterId !== clusterId) return + bus._emit('#stream', packet, ...args) } - bus.discovered = new Promise(resolve => bus.once('#ready', resolve)) - bus.peer = peer + _peer.onData = (...args) => bus._emit('#data', ...args) + _peer.onSend = (...args) => bus._emit('#send', ...args) + _peer.onMulticast = (...args) => bus._emit('#multicast', ...args) + _peer.onConnection = (...args) => bus._emit('#connection', ...args) + _peer.onDisconnection = (...args) => bus._emit('#disconnection', ...args) + _peer.onQuery = (...args) => bus._emit('#query', ...args) + _peer.onNat = (...args) => bus._emit('#network-change', ...args) + _peer.onError = (...args) => bus._emit('#error', ...args) + _peer.onWarn = (...args) => bus._emit('#warning', ...args) + _peer.onState = (...args) => bus._emit('#state', ...args) + _peer.onConnecting = (...args) => bus._emit('#connecting', ...args) + + bus.discovered = new Promise(resolve => bus._once('#discovered', resolve)) + bus.peer = _peer + bus.peerId = _peer.peerId bus.setMaxListeners(1024) bus.subclusters = new Map() + bus.address = () => ({ + address: _peer.address, + port: _peer.port, + natType: NAT.toString(_peer.natType) + }) + + bus.join = () => _peer.requestReflection() + bus.cacheSize = () => _peer.cache.size * PACKET_BYTES + bus.open = (m, pk) => _peer.encryption.open(m, pk) + + bus.emit = async (...args) => { + return await _peer.publish(options.sharedKey, await pack(...args)) + } + + bus.on = async (eventName, cb) => { + if (eventName[0] !== '#') eventName = await sha256(eventName) + bus._on(eventName, cb) + } + + _peer.onReady = () => { + _peer.isReady = true + bus._emit('#discovered') + } + + const pack = async (sub, eventName, value, opts = {}) => { + if (typeof eventName !== 'string') throw new Error('event name must be a string') + if (eventName.length === 0) throw new Error('event name too short') + + opts.ttl = Math.min(opts.ttl, CACHE_TTL) + + const args = { + clusterId, + ...opts, + usr1: await sha256(eventName, { bytes: true }) + } + + if (!isBufferLike(value) && typeof value === 'object') { + try { + args.message = Buffer.from(JSON.stringify(value)) + } catch (err) { + return bus._emit('error', err) + } + } else { + args.message = Buffer.from(value) + } + + if (opts.publicKey && opts.privateKey) { + args.usr2 = Buffer.from(opts.publicKey) + args.sig = Buffer.from(Encryption.sign(args.message, Buffer.from(opts.privateKey))) + } + + return args + } + + const unpack = async packet => { + let opened + let verified + const sub = bus.subclusters.get(packet.subclusterId) + if (!sub) return {} + + try { + opened = _peer.encryption.open(packet.message, packet.subclusterId) + } catch (err) { + sub._emit('warning', err) + return {} + } + + if (packet.sig) { + try { + if (Encryption.verify(opened, packet.sig, packet.usr2)) { + verified = true + } + } catch (err) { + sub._emit('warning', err) + return {} + } + } + + return { opened, verified } + } + + // + // Creates a peer if one doesn't exist, adds it to the right subcluster + // and cluster. + // + const getPeerRepresentative = (packet, sub, peer) => { + let ee + + if (sub && sub.peers.has(peer.peerId)) { + ee = sub.peers.get(peer.peerId) + } else { + ee = new events.EventEmitter() + ee._on = ee.on + ee._once = ee.once + ee._emit = ee.emit + ee.peerId = peer.peerId + ee.address = peer.address + ee.port = peer.port + } + + if (!bus.peers.has(peer.peerId)) { + bus.peers.set(peer.peerId, ee) + } + + if (!sub) return ee + sub.peers.set(peer.peerId, ee) + + return ee + } + /* * Creates an event emitter that will handle inbound and outbound * events as well as connects for the specified subcluster. @@ -75,238 +201,127 @@ export default (dgram, events) => { * * @example * - *```js + * ```js * import fs from '../fs.js' * import { network, Encryption } from '../network.js' * - * const oldState = parse(fs.readFile('state.json')) - * - * const sharedKey = await Encryption.createSharedKey() - * - * const socket = network(oldState) - * const cats = await socket.subcluster({ sharedKey }) + * const clusterId = await Encryption.createClusterId() + * const socket = network({ clusterId }) * - * console.log(cats.sharedKey) + * socket.emit('foo', value) // publish a message to anyone in this cluster + * socket.on('foo', () => {}) // read published messags from anyone in this cluster * - * cats.emit('mew', value) + * const sharedKey = await Encryption.createSharedKey() + * const sub = await socket.subcluster({ sharedKey }) * - * cats.on('mew', value => { - * console.log(value) - * }) + * sub.emit('foo', value) // publish a message to anyone in this cluster and this subcluster + * sub.on('foo', () => {}) // read streamed messages from anyone in this cluster and this subcluster * - * cats.on('#connection', cat => { - * cat.emit('mew', value) + * sub.on('#join', peer => { // subscribe to a specific peer in the subcluster + * peer.emit('foo', value) // send an event named 'foo' only to peer * - * cat.on('mew', value => { + * peer.on('foo', value => { // listen for events named 'foo' only from peer * console.log(value) * }) * }) - * - * fs.writeFile('state.json', stringify(socket.getState())) - *``` + * ``` */ bus.subcluster = async (options = {}) => { + if (!options.sharedKey) throw new Error('expected options.sharedKey to be of type Uint8Array') + const keys = await Encryption.createKeyPair(options.sharedKey) const subclusterId = Buffer.from(keys.publicKey).toString('base64') if (bus.subclusters.has(subclusterId)) return bus.subclusters.get(subclusterId) - const selfEvents = new events.EventEmitter() - const emitSelf = selfEvents.emit - const onSelf = selfEvents.on - - selfEvents.sharedKey = options.sharedKey - selfEvents.keys = keys - - peer.encryption.add(keys.publicKey, keys.privateKey) - bus.subclusters.set(subclusterId, selfEvents) + const sub = new events.EventEmitter() + bus.subclusters.set(subclusterId, sub) - selfEvents.subclusterId = subclusterId - selfEvents.peers = new Map() - selfEvents.peerId = peer.peerId + sub._emit = sub.emit + sub._on = sub.on + sub.peers = new Map() + sub.peerId = _peer.peerId + sub.subclusterId = subclusterId + sub.sharedKey = options.sharedKey + sub.keys = keys - selfEvents.on = async (eventName, cb) => { - if (eventName[0] !== '#') eventName = await sha256(eventName) - onSelf.call(selfEvents, eventName, cb) + sub.emit = async (eventName, ...args) => { + return await _peer.publish(sub.sharedKey, await pack(sub, eventName, ...args)) } - const onBeforeEmit = async (eventName, value, opts) => { - if (typeof eventName !== 'string') throw new Error('event name must be a string') - if (eventName.length === 0) throw new Error('event name too short') - - const args = { - clusterId, - subclusterId, - usr1: await sha256(eventName, { bytes: true }) - } - - if (isBufferLike(value) || typeof value === 'string') { - args.message = value - } else { - try { - args.message = JSON.stringify(value) - } catch (err) { - return bus.emit('error', err) - } - } - - args.message = Buffer.from(args.message) - - if (opts.publicKey && opts.privateKey) { - args.usr2 = Buffer.from(opts.publicKey) - args.sig = Buffer.from(Encryption.sign(args.message, Buffer.from(opts.privateKey))) - } - - args.previousId = opts.previousId - args.ttl = opts.ttl ?? 0 - return args - } - - selfEvents.emit = async (...args) => { - return await peer.publish(options.sharedKey, await onBeforeEmit(...args)) + sub.on = async (eventName, cb) => { + if (eventName[0] !== '#') eventName = await sha256(eventName) + sub._on(eventName, cb) } - bus.on('#connection', (packet, p) => { - if (selfEvents.peers.has(p.peerId)) return - - const peerEvents = selfEvents.peers.get(p.peerId) || new events.EventEmitter() - const emitPeer = peerEvents.emit - const onPeer = peerEvents.on - - const handleStreamedPacket = (packet, p) => { - if (packet?.subclusterId !== subclusterId) return - if (packet?.streamFrom !== p.peerId) return - if (packet?.streamTo !== peer.peerId) return - - const eventName = packet.usr1.toString('hex') - let opened - - try { - opened = peer.encryption.open(packet.message, packet.subclusterId) - } catch (err) { - return emitPeer.call(peerEvents, '#error', err) - } - - if (packet.sig) { - try { - if (Encryption.verify(opened, packet.sig, packet.usr2)) { - packet.verified = true - } - } catch {} - } - - emitPeer.call(peerEvents, eventName, opened, packet) - } - - if (!selfEvents.peers.has(p.peerId)) { - peerEvents.peerId = p.peerId - - selfEvents.peers.set(p.peerId, peerEvents) - - peerEvents.on = async (eventName, cb) => { - if (eventName[0] !== '#') eventName = await sha256(eventName) - onPeer.call(peerEvents, eventName, cb) - } - - peerEvents.emit = async (...args) => { - return p.write(options.sharedKey, await onBeforeEmit(...args)) - } - - bus.once('#disconnection', (packet, peer) => { - emitSelf.call(selfEvents, '#disconnection', peerEvents) - }) - - bus.on('#stream', handleStreamedPacket) - } - - emitSelf.call(selfEvents, '#connection', peerEvents, packet) - - if (packet.type === PacketStream.type) { - queueMicrotask(() => handleStreamedPacket(packet, p)) - } - }) - - bus.on('#stream', (packet, p) => { - if (selfEvents.peers.has(p.peerId)) return - bus.emit('#connection', packet, p) - }) - - bus.on('#query', (packet, ...args) => { - emitSelf.call(selfEvents, '#query', packet) - }) - - bus.on('#network-change', (packet, ...args) => { - emitSelf.call(selfEvents, '#network-change', packet) - }) - - bus.on('#state', (packet, ...args) => { - emitSelf.call(selfEvents, '#state', packet) - }) - - bus.on('#error', (...args) => { - emitSelf.call(selfEvents, '#error', ...args) - }) - - bus.on('#packet', (packet, ...args) => { - if (packet.subclusterId !== subclusterId) return - const eventName = packet.usr1.toString('hex') - - let opened - - try { - opened = peer.encryption.open(packet.message, packet.subclusterId) - } catch (err) { - return emitSelf.call(selfEvents, '#error', err) - } - - if (packet.sig) { - try { - if (Encryption.verify(opened, packet.sig, packet.usr2)) { - packet.verified = true - } - } catch {} - } - emitSelf.call(selfEvents, eventName, opened, packet) - }) - - bus.on('#data', (packet, port, address) => { - const p = peer.peers.find(p => p.port === port && p.address === address) - if (!p) return - - if (!p.received) p.received = 0 - p.received += 1 + bus._on('#disconnection', peer => { + sub._emit('#leave', peer) + sub.peers.delete(peer.peerId) + bus.peers.delete(peer.peerId) }) - bus.on('#ready', () => { - peer.join(options.sharedKey, { subclusterId, ...options }) + bus._on('#discovered', () => { + _peer.join(sub.sharedKey, { subclusterId, ...options }) + sub._emit('#ready', bus.address()) }) - return selfEvents + _peer.join(sub.sharedKey, { subclusterId, ...options }) + return sub } - bus.ready = () => { - peer.requestReflection() - if (peer.isReady) bus.emit('#ready') - } - - bus.getState = () => { - return peer.getState() - } + bus._on('#join', async (packet, peer) => { + const sub = bus.subclusters.get(packet.subclusterId) + if (!sub) return - bus.natType = () => { - return NAT.toString(peer.natType) - } + const ee = getPeerRepresentative(packet, sub, peer) - bus.cacheSize = () => { - return peer.cache.size - } - - bus.open = packet => { - return peer.encryption.open(packet.message, packet.subclusterId) - } + ee.emit = async (eventName, ...args) => { + return peer.write && peer.write(sub.sharedKey, await pack(sub, eventName, ...args)) + } - await peer.init() + ee.on = async (eventName, cb) => { + if (eventName[0] !== '#') eventName = await sha256(eventName) + ee._on(eventName, cb) + } + if (ee) { + ee.peerId = peer.peerId + ee.address = peer.address + sub._emit('#join', ee, packet) + } + }) + + bus._on('#packet', async (packet, peer) => { + const sub = bus.subclusters.get(packet.subclusterId) + if (!sub) return + + const exists = sub.peers.has(peer.peerId) + const ee = getPeerRepresentative(packet, sub, peer) + if (!exists) bus._emit('#join', packet, peer) + + const eventName = packet.usr1.toString('hex') + const { verified, opened } = await unpack(packet) + if (verified) packet.verified = true + sub._emit(eventName, opened, packet) + ee._emit(eventName, opened, packet) + }) + + bus._on('#stream', async (packet, peer, port, address) => { + const sub = bus.subclusters.get(packet.subclusterId) + if (!sub) return + + const exists = sub.peers.has(peer.peerId) + const ee = getPeerRepresentative(packet, sub, peer) + if (!exists) bus._emit('#join', packet, peer) + + const eventName = packet.usr1.toString('hex') + const { verified, opened } = await unpack(packet) + if (verified) packet.verified = true + sub._emit(eventName, opened, packet) + ee._emit(eventName, opened, packet) + }) + + await _peer.init() return bus } } From 6539a62c505a11edc320f80c37054603aca7a404 Mon Sep 17 00:00:00 2001 From: heapwolf Date: Mon, 2 Oct 2023 19:23:00 +0200 Subject: [PATCH 003/256] generate docs and types --- api/index.d.ts | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/api/index.d.ts b/api/index.d.ts index 13409c9e22..56d01a91e0 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -5132,7 +5132,7 @@ declare module "socket:stream-relay/packets" { /** * The version of the protocol. */ - export const VERSION: 3; + export const VERSION: 4; /** * The size in bytes of the prefix magic bytes. */ @@ -5295,22 +5295,27 @@ declare module "socket:stream-relay/packets" { } export class PacketIntro extends Packet { static type: number; - constructor({ clock, message }: { + constructor({ clock, hops, clusterId, subclusterId, message }: { clock: any; + hops: any; + clusterId: any; + subclusterId: any; message: any; }); } export class PacketJoin extends Packet { static type: number; - constructor({ clock, clusterId, message }: { + constructor({ clock, hops, clusterId, subclusterId, message }: { clock: any; + hops: any; clusterId: any; + subclusterId: any; message: any; }); } export class PacketPublish extends Packet { static type: number; - constructor({ message, sig, packetId, clusterId, subclusterId, nextId, clock, to, usr1, usr2, ttl, previousId }: { + constructor({ message, sig, packetId, clusterId, subclusterId, nextId, clock, hops, to, usr1, usr2, ttl, previousId }: { message: any; sig: any; packetId: any; @@ -5318,6 +5323,7 @@ declare module "socket:stream-relay/packets" { subclusterId: any; nextId: any; clock: any; + hops: any; to: any; usr1: any; usr2: any; @@ -5364,9 +5370,9 @@ declare module "socket:stream-relay/packets" { declare module "socket:stream-relay/encryption" { export class Encryption { static createSharedKey(seed: any): Promise; - static createClusterId(sharedKey: any): Promise; static createKeyPair(seed: any): Promise; static createId(str?: Buffer): Promise; + static createClusterId(value: any): Promise; static createSubclusterId(value: any): Promise; /** * @param {Buffer} b - The message to sign @@ -5759,7 +5765,9 @@ declare module "socket:stream-relay/index" { isListening: boolean; ctime: number; lastUpdate: number; + lastSync: number; closing: boolean; + clock: number; unpublished: {}; cache: any; uptime: number; @@ -5771,6 +5779,16 @@ declare module "socket:stream-relay/index" { rates: Map; streamBuffer: Map; controlPackets: Map; + metrics: { + 0: number; + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; + 6: number; + 7: number; + }; peers: any; encryption: Encryption; config: any; @@ -5844,20 +5862,20 @@ declare module "socket:stream-relay/index" { * @return {Array} * @ignore */ - getPeers(packet: any, peers: any, n?: number, filter?: (o: any) => any): Array; + getPeers(packet: any, peers: any, ignorelist: any, filter?: (o: any) => any): Array; /** * Send an eventually consistent packet to a selection of peers (fanout) * @return {undefined} * @ignore */ - mcast(packet: any, packetId: any, isTaxed: any, ignorelist?: any[]): undefined; + mcast(packet: any, isTaxed: any, ignorelist?: any[]): undefined; /** * The process of determining this peer's NAT behavior (firewall and dependentness) * @return {undefined} * @ignore */ requestReflection(): undefined; - probeReflectionTimeout: number; + probeReflectionTimeout: any; /** * Ping another peer * @return {PacketPing} @@ -5884,11 +5902,11 @@ declare module "socket:stream-relay/index" { * This should be called at least once when an app starts to multicast * this peer, and starts querying the network to discover peers. * @param {object} keys - Created by `Encryption.createKeyPair()`. - * @param {object=} opts - Options - * @param {number=MAX_BANDWIDTH} opts.rateLimit - How many requests per second to allow for this subclusterId. + * @param {object=} args - Options + * @param {number=MAX_BANDWIDTH} args.rateLimit - How many requests per second to allow for this subclusterId. * @return {RemotePeer} */ - join(sharedKey: any, opts?: object | undefined): RemotePeer; + join(sharedKey: any, args?: object | undefined): RemotePeer; /** * @param {Packet} T - The constructor to be used to create packets. * @param {Any} message - The message to be split and packaged. @@ -5957,6 +5975,7 @@ declare module "socket:stream-relay/index" { * @ignore */ _onIntro(packet: any, port: any, address: any): undefined; + socketPool: any[]; /** * Received an Join Packet * @return {undefined} @@ -5996,7 +6015,7 @@ declare module "socket:stream-relay/index" { _onMessage(data: Buffer | Uint8Array, { port, address }: { port: number; address: string; - }): undefined; + }): Promise; }; }; export default wrap; From 7e14d713c171c97fb51eb587da6121e6425b6634 Mon Sep 17 00:00:00 2001 From: Bret Comnes <166301+bcomnes@users.noreply.github.com> Date: Mon, 2 Oct 2023 11:57:22 -0700 Subject: [PATCH 004/256] Backport tapzero plan (#627) * Backport tapzero plan Backport https://github.com/socketsupply/tapzero/pull/16 * Re-run gen * Backport tapzero #19 https://github.com/socketsupply/tapzero/pull/19/files * Delete README So we don't have to maintain a duplicate. --- api/README.md | 85 ++++++++++++++++++--------------- api/index.d.ts | 17 +++++++ api/test/README.md | 114 --------------------------------------------- api/test/index.js | 40 ++++++++++++++++ 4 files changed, 104 insertions(+), 152 deletions(-) delete mode 100644 api/test/README.md diff --git a/api/README.md b/api/README.md index f6fc07dbd0..d99a4493d6 100644 --- a/api/README.md +++ b/api/README.md @@ -1550,7 +1550,7 @@ This is a `FunctionDeclaration` named `memoryUsage` in `api/process.js`, it's ex | fn | TestFn | | false | | | runner | TestRunner | | false | | -### [`comment(msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L117) +### [`comment(msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L127) @@ -1558,7 +1558,16 @@ This is a `FunctionDeclaration` named `memoryUsage` in `api/process.js`, it's ex | :--- | :--- | :---: | :---: | :--- | | msg | string | | false | | -### [`deepEqual(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L128) +### [`plan(n)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L137) + +Plan the number of assertions. + + +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| n | number | | false | | + +### [`deepEqual(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L148) @@ -1568,7 +1577,7 @@ This is a `FunctionDeclaration` named `memoryUsage` in `api/process.js`, it's ex | expected | T | | false | | | msg | string | | false | | -### [`notDeepEqual(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L143) +### [`notDeepEqual(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L163) @@ -1578,7 +1587,7 @@ This is a `FunctionDeclaration` named `memoryUsage` in `api/process.js`, it's ex | expected | T | | false | | | msg | string | | false | | -### [`equal(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L158) +### [`equal(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L178) @@ -1588,7 +1597,7 @@ This is a `FunctionDeclaration` named `memoryUsage` in `api/process.js`, it's ex | expected | T | | false | | | msg | string | | false | | -### [`notEqual(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L173) +### [`notEqual(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L193) @@ -1598,7 +1607,7 @@ This is a `FunctionDeclaration` named `memoryUsage` in `api/process.js`, it's ex | expected | unknown | | false | | | msg | string | | false | | -### [`fail(msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L186) +### [`fail(msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L206) @@ -1606,7 +1615,7 @@ This is a `FunctionDeclaration` named `memoryUsage` in `api/process.js`, it's ex | :--- | :--- | :---: | :---: | :--- | | msg | string | | false | | -### [`ok(actual, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L199) +### [`ok(actual, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L219) @@ -1615,7 +1624,7 @@ This is a `FunctionDeclaration` named `memoryUsage` in `api/process.js`, it's ex | actual | unknown | | false | | | msg | string | | false | | -### [`pass(msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L211) +### [`pass(msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L231) @@ -1623,7 +1632,7 @@ This is a `FunctionDeclaration` named `memoryUsage` in `api/process.js`, it's ex | :--- | :--- | :---: | :---: | :--- | | msg | string | | false | | -### [`ifError(err, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L220) +### [`ifError(err, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L240) @@ -1632,7 +1641,7 @@ This is a `FunctionDeclaration` named `memoryUsage` in `api/process.js`, it's ex | err | Error \| null \| undefined | | false | | | msg | string | | false | | -### [`throws(fn, expected, message)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L233) +### [`throws(fn, expected, message)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L253) @@ -1642,7 +1651,7 @@ This is a `FunctionDeclaration` named `memoryUsage` in `api/process.js`, it's ex | expected | RegExp \| any | | false | | | message | string | | false | | -### [`sleep(ms, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L282) +### [`sleep(ms, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L302) Sleep for ms with an optional msg @@ -1660,7 +1669,7 @@ Sleep for ms with an optional msg | :--- | :--- | :--- | | Not specified | Promise | | -### [`requestAnimationFrame(msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L300) +### [`requestAnimationFrame(msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L320) Request animation frame with an optional msg. Falls back to a 0ms setTimeout when tests are run headlessly. @@ -1678,7 +1687,7 @@ Request animation frame with an optional msg. Falls back to a 0ms setTimeout whe | :--- | :--- | :--- | | Not specified | Promise | | -### [`click(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L322) +### [`click(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L342) Dispatch the `click`` method on an element specified by selector. @@ -1696,7 +1705,7 @@ Dispatch the `click`` method on an element specified by selector. | :--- | :--- | :--- | | Not specified | Promise | | -### [`eventClick(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L344) +### [`eventClick(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L364) Dispatch the click window.MouseEvent on an element specified by selector. @@ -1714,7 +1723,7 @@ Dispatch the click window.MouseEvent on an element specified by selector. | :--- | :--- | :--- | | Not specified | Promise | | -### [`dispatchEvent(event, target, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L372) +### [`dispatchEvent(event, target, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L392) Dispatch an event on the target. @@ -1733,7 +1742,7 @@ Dispatch an event on the target. | :--- | :--- | :--- | | Not specified | Promise | | -### [`focus(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L392) +### [`focus(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L412) Call the focus method on element specified by selector. @@ -1751,7 +1760,7 @@ Call the focus method on element specified by selector. | :--- | :--- | :--- | | Not specified | Promise | | -### [`blur(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L413) +### [`blur(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L433) Call the blur method on element specified by selector. @@ -1769,7 +1778,7 @@ Call the blur method on element specified by selector. | :--- | :--- | :--- | | Not specified | Promise | | -### [`type(selector, str, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L435) +### [`type(selector, str, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L455) Consecutively set the str value of the element specified by selector to simulate typing. @@ -1788,7 +1797,7 @@ Consecutively set the str value of the element specified by selector to simulate | :--- | :--- | :--- | | Not specified | Promise | | -### [`appendChild(parentSelector, el, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L467) +### [`appendChild(parentSelector, el, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L487) appendChild an element el to a parent selector element. @@ -1808,7 +1817,7 @@ appendChild an element el to a parent selector element. | :--- | :--- | :--- | | Not specified | Promise | | -### [`removeElement(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L487) +### [`removeElement(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L507) Remove an element from the DOM. @@ -1826,7 +1835,7 @@ Remove an element from the DOM. | :--- | :--- | :--- | | Not specified | Promise | | -### [`elementVisible(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L506) +### [`elementVisible(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L526) Test if an element is visible @@ -1844,7 +1853,7 @@ Test if an element is visible | :--- | :--- | :--- | | Not specified | Promise | | -### [`elementInvisible(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L527) +### [`elementInvisible(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L547) Test if an element is invisible @@ -1862,7 +1871,7 @@ Test if an element is invisible | :--- | :--- | :--- | | Not specified | Promise | | -### [`waitFor(querySelectorOrFn, opts, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L551) +### [`waitFor(querySelectorOrFn, opts, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L571) Test if an element is invisible @@ -1883,7 +1892,7 @@ Test if an element is invisible | :--- | :--- | :--- | | Not specified | Promise | | -### [`waitForText(selector, opts, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L613) +### [`waitForText(selector, opts, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L633) Test if an element is invisible @@ -1913,7 +1922,7 @@ Test if an element is invisible | :--- | :--- | :--- | | Not specified | Promise | | -### [`querySelector(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L650) +### [`querySelector(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L670) Run a querySelector as an assert and also get the results @@ -1931,7 +1940,7 @@ Run a querySelector as an assert and also get the results | :--- | :--- | :--- | | Not specified | HTMLElement \| Element | | -### [`querySelectorAll(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L669) +### [`querySelectorAll(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L689) Run a querySelectorAll as an assert and also get the results @@ -1949,7 +1958,7 @@ Run a querySelectorAll as an assert and also get the results | :--- | :--- | :--- | | Not specified | Array | | -### [`getComputedStyle(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L698) +### [`getComputedStyle(selector, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L718) Retrieves the computed styles for a given element. @@ -1974,17 +1983,17 @@ Retrieves the computed styles for a given element. | :--- | :--- | :--- | | Not specified | CSSStyleDeclaration | The computed styles of the element. | -### [`run()`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L787) +### [`run()`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L815) pass: number, fail: number }>} -## [TestRunner](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L856) +## [TestRunner](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L896) -### [`constructor(report)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L861) +### [`constructor(report)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L901) @@ -1992,7 +2001,7 @@ Retrieves the computed styles for a given element. | :--- | :--- | :---: | :---: | :--- | | report | (lines: string) => void | | false | | -### [`nextId()`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L913) +### [`nextId()`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L953) @@ -2000,7 +2009,7 @@ Retrieves the computed styles for a given element. | :--- | :--- | :--- | | Not specified | string | | -### [`add(name, fn, only)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L923) +### [`add(name, fn, only)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L963) @@ -2010,7 +2019,7 @@ Retrieves the computed styles for a given element. | fn | TestFn | | false | | | only | boolean | | false | | -### [`run()`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L945) +### [`run()`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L985) @@ -2018,7 +2027,7 @@ Retrieves the computed styles for a given element. | :--- | :--- | :--- | | Not specified | Promise | | -### [`onFinish() )`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L992) +### [`onFinish() )`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1032) @@ -2026,12 +2035,12 @@ Retrieves the computed styles for a given element. | :--- | :--- | :---: | :---: | :--- | | ) | (result: { total: number, success: number, fail: number | > void} callback | false | | -## [GLOBAL_TEST_RUNNER](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1008) +## [GLOBAL_TEST_RUNNER](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1048) This is a `VariableDeclaration` named `GLOBAL_TEST_RUNNER` in `api/test/index.js`, it's exported but undocumented. -## [`only(name, fn)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1017) +## [`only(name, fn)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1057) @@ -2040,7 +2049,7 @@ This is a `VariableDeclaration` named `GLOBAL_TEST_RUNNER` in `api/test/index.js | name | string | | false | | | fn | TestFn | | false | | -## [`skip(_name, _fn)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1027) +## [`skip(_name, _fn)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1067) @@ -2049,7 +2058,7 @@ This is a `VariableDeclaration` named `GLOBAL_TEST_RUNNER` in `api/test/index.js | _name | string | | false | | | _fn | TestFn | | false | | -## [`setStrict(strict)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1033) +## [`setStrict(strict)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1073) diff --git a/api/index.d.ts b/api/index.d.ts index 56d01a91e0..73edbb8b4c 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -4411,6 +4411,16 @@ declare module "socket:test/index" { * @ignore */ name: string; + /** + * @type {null|number} + * @ignore + */ + _planned: null | number; + /** + * @type {null|number} + * @ignore + */ + _actual: null | number; /** * @type {TestFn} * @ignore @@ -4444,6 +4454,13 @@ declare module "socket:test/index" { * @returns {void} */ comment(msg: string): void; + /** + * Plan the number of assertions. + * + * @param {number} n + * @returns {void} + */ + plan(n: number): void; /** * @template T * @param {T} actual diff --git a/api/test/README.md b/api/test/README.md deleted file mode 100644 index 9f3cd1a690..0000000000 --- a/api/test/README.md +++ /dev/null @@ -1,114 +0,0 @@ -# tapzero - -Zero dependency test framework - -## Source code - -The implementation is <250 loc, (<500 with comments) ( https://github.com/Raynos/tapzero/blob/master/index.js ) and very readable. - -## Migrating from tape - -```js -const test = require('tape') -// Tapzero exports an object with a test function property. -const test = require('tapzero').test -``` - -```js -tape('my test', (t) => { - t.equal(2, 2, 'ok') - t.end() -}) -// Auto ending behavior on function completion -tapzero('my test', (t) => { - t.equal(2, 2, 'ok') - // t.end() does not exist. -}) -``` - -```js -// tapzero "auto" ends async tests when the async function completes -tapzero('my cb test', async (t) => { - await new Promise((resolve) => { - t.equal(2, 2, 'ok') - setTimeout(() => { - // instead of calling t.end(), resolve a promise - resolve() - }, 10) - }) -}) -``` - -```js -tape('my test', (t) => { - t.equals(2, 2) - t.is(2, 2) - t.isEqual(2, 2) -}) -tapzero('my test', (t) => { - // tapzero does not implement any aliases, very small surface area. - t.equal(2, 2) - t.equal(2, 2) - t.equal(2, 2) -}) -``` - -## Motivation - -Small library, zero dependencies - -``` -$ package-size ./build/src/index.js zora baretest,assert qunit tape jasmine mocha - - package size minified gzipped - ./build/src/index.js 8.97 KB 3.92 KB 1.53 KB - zora@3.1.8 32.44 KB 11.65 KB 4.08 KB - baretest@1.0.0,assert@2.0.0 51.61 KB 16.48 KB 5.82 KB - qunit@2.9.3 195.83 KB 62.04 KB 20.38 KB - tape@4.13.0 304.71 KB 101.46 KB 28.8 KB - jasmine@3.5.0 413.61 KB 145.2 KB 41.07 KB - mocha@7.0.1 811.55 KB 273.07 KB 91.61 KB - -``` - -Small library, small install size. - -| | tapzero | baretest | zora | pta | tape | -|--------|:---------:|:----------:|:------:|:-----:|:------:| -|pkg size| [![tapzero](https://packagephobia.now.sh/badge?p=tapzero@0.1.1)](https://packagephobia.now.sh/result?p=tapzero) | [![baretest](https://packagephobia.now.sh/badge?p=baretest)](https://packagephobia.now.sh/result?p=baretest) | [![zora](https://packagephobia.now.sh/badge?p=zora)](https://packagephobia.now.sh/result?p=zora) | [![pta](https://packagephobia.now.sh/badge?p=pta)](https://packagephobia.now.sh/result?p=pta) | [![tape](https://packagephobia.now.sh/badge?p=tape)](https://packagephobia.now.sh/result?p=tape) | -|Min.js size| [![tapzero](https://badgen.net/bundlephobia/min/tapzero)](https://bundlephobia.com/result?p=tapzero) | [![baretest](https://badgen.net/bundlephobia/min/baretest)](https://bundlephobia.com/result?p=baretest) | [![zora](https://badgen.net/bundlephobia/min/zora)](https://bundlephobia.com/result?p=zora) | [![pta](https://badgen.net/bundlephobia/min/pta)](https://bundlephobia.com/result?p=pta) | [![tape](https://badgen.net/bundlephobia/min/tape)](https://bundlephobia.com/result?p=tape) | -|dep count| [![tapzero](https://badgen.net/badge/dependencies/0/green)](https://www.npmjs.com/package/tapzero) | [![baretest](https://badgen.net/badge/dependencies/1/green)](https://www.npmjs.com/package/baretest) | [![zora](https://badgen.net/badge/dependencies/0/green)](https://www.npmjs.com/package/zora) | [![pta](https://badgen.net/badge/dependencies/23/orange)](https://www.npmjs.com/package/pta) | [![tape](https://badgen.net/badge/dependencies/44/orange)](https://www.npmjs.com/package/tape) | - -| | Mocha | Ava | Jest | tap | -|:------:|:-------:|:-----:|:------:|:-----:| -|pkg size| [![mocha](https://packagephobia.now.sh/badge?p=mocha)](https://packagephobia.now.sh/result?p=mocha) | [![ava](https://packagephobia.now.sh/badge?p=ava)](https://packagephobia.now.sh/result?p=ava) | [![jest](https://packagephobia.now.sh/badge?p=jest)](https://packagephobia.now.sh/result?p=jest) | [![tap](https://packagephobia.now.sh/badge?p=tap)](https://packagephobia.now.sh/result?p=tap) | -|Min.js size| [![mocha](https://badgen.net/bundlephobia/min/mocha)](https://bundlephobia.com/result?p=mocha) | [![ava](https://badgen.net/bundlephobia/min/ava)](https://bundlephobia.com/result?p=ava) | [![jest](https://badgen.net/bundlephobia/min/jest)](https://bundlephobia.com/result?p=jest) | [![tap](https://badgen.net/bundlephobia/min/tap)](https://bundlephobia.com/result?p=tap) | -|dep count| [![mocha](https://badgen.net/badge/dependencies/104/red)](https://www.npmjs.com/package/mocha) | [![ava](https://badgen.net/badge/dependencies/300/red)](https://www.npmjs.com/package/ava) | [![jest](https://badgen.net/badge/dependencies/799/red)](https://www.npmjs.com/package/jest) | [![tap](https://badgen.net/badge/dependencies/390/red)](https://www.npmjs.com/package/tap) | - -## Docs - -```js -const test = require('tapzero').test -``` - -### `test(name, [fn])` - -Run a single named test case. The `fn` will be called with the `t` test object. - -Tests run one at a time and complete when the `fn` completes, the `fn` can be async. - -### `test.only(name, fn)` - -Like `test(name, fn)` except if you use `.only` this is the only test case that will run for the entire process, all other test cases using tape will be ignored. - -### `test.skip(name, [fn])` - -Creates a test case that will be skipped - -## Harness docs - -```js -const testHarness = require('tapzero/harness') -``` - -See [HARNESS.md](./HARNESS.md) diff --git a/api/test/index.js b/api/test/index.js index fd8ee3bc48..5f0b011ee4 100644 --- a/api/test/index.js +++ b/api/test/index.js @@ -79,6 +79,16 @@ export class Test { * @ignore */ this.name = name + /** + * @type {null|number} + * @ignore + */ + this._planned = null + /** + * @type {null|number} + * @ignore + */ + this._actual = null /** * @type {TestFn} * @ignore @@ -118,6 +128,16 @@ export class Test { this.runner.report('# ' + msg) } + /** + * Plan the number of assertions. + * + * @param {number} n + * @returns {void} + */ + plan (n) { + this._planned = n + } + /** * @template T * @param {T} actual @@ -729,6 +749,14 @@ export class Test { ) } + if (this._planned !== null) { + this._actual = ((this._actual || 0) + 1) + + if (this._actual > this._planned) { + throw new Error(`More tests than planned in TEST *${this.name}*`) + } + } + const report = this.runner.report const prefix = pass ? 'ok' : 'not ok' @@ -790,7 +818,19 @@ export class Test { if (maybeP && typeof maybeP.then === 'function') { await maybeP } + this.done = true + + if (this._planned !== null) { + if (this._planned > (this._actual || 0)) { + throw new Error(`Test ended before the planned number + planned: ${this._planned} + actual: ${this._actual || 0} + ` + ) + } + } + return this._result } } From 11c6352d25dbcc21c685d33788327cf48cd0e821 Mon Sep 17 00:00:00 2001 From: Joseph Werle Date: Fri, 29 Sep 2023 10:02:52 -0400 Subject: [PATCH 005/256] refactor(bin/cflags.sh): use 'fPIC' on darwin --- bin/cflags.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/cflags.sh b/bin/cflags.sh index bf1be671c4..319ce43a69 100755 --- a/bin/cflags.sh +++ b/bin/cflags.sh @@ -82,6 +82,7 @@ fi if (( !TARGET_OS_ANDROID && !TARGET_ANDROID_EMULATOR )); then if [[ "$host" = "Darwin" ]]; then cflags+=("-ObjC++") + cflags+=("-fPIC") elif [[ "$host" = "Linux" ]]; then cflags+=($(pkg-config --cflags --static gtk+-3.0 webkit2gtk-4.1) -fPIC) elif [[ "$host" = "Win32" ]]; then From 18e21c399a2f0100267714b8d4c80d0c7d266b9d Mon Sep 17 00:00:00 2001 From: Joseph Werle Date: Fri, 29 Sep 2023 10:59:37 -0400 Subject: [PATCH 006/256] refactor(src/ipc/bridge.cc): internal linkage for 'initRouterTable' --- src/ipc/bridge.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ipc/bridge.cc b/src/ipc/bridge.cc index 37a6d23e1a..e84150846d 100644 --- a/src/ipc/bridge.cc +++ b/src/ipc/bridge.cc @@ -128,7 +128,7 @@ static String getcwd () { } \ } -void initRouterTable (Router *router) { +static void initRouterTable (Router *router) { static auto userConfig = SSC::getUserConfig(); #if defined(__APPLE__) static auto bundleIdentifier = userConfig["meta_bundle_identifier"]; From f8fe14fd3e6e7c03699167270cf34807481bf6b2 Mon Sep 17 00:00:00 2001 From: Joseph Werle Date: Fri, 29 Sep 2023 10:59:58 -0400 Subject: [PATCH 007/256] refactor(src/cli/cli.cc): remove '-lsocket-runtime' --- src/cli/cli.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 90c3390065..bc4e4a2af4 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -4775,7 +4775,6 @@ int main (const int argc, const char* argv[]) { << " -Wno-nonportable-include-path" #else << (" -L" + quote + trim(prefixFile("lib/" + platform.arch + "-desktop")) + quote) - << " -lsocket-runtime" #endif << " -fvisibility=hidden" << " -DIOS=0" From b801fc84db1cd9238b3c37dda8ae3b497d20b284 Mon Sep 17 00:00:00 2001 From: Joseph Werle Date: Fri, 29 Sep 2023 10:45:39 -0400 Subject: [PATCH 008/256] chore(test): fix android tests --- test/src/dgram.js | 2 ++ test/src/fs/promises.js | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/test/src/dgram.js b/test/src/dgram.js index 812acd014e..0ac20cc784 100644 --- a/test/src/dgram.js +++ b/test/src/dgram.js @@ -6,6 +6,7 @@ import crypto from 'socket:crypto' import Buffer from 'socket:buffer' import dgram from 'socket:dgram' import util from 'socket:util' +import os from 'socket:os' // node compat /* @@ -118,6 +119,7 @@ test('dgram createSocket, address, bind, close', async (t) => { test('udp bind, send, remoteAddress', async (t) => { if (process.env.SSC_ANDROID_CI) return + if (os.platform() === 'win32' && process.env.GITHUB_ACTIONS_CI) return const server = dgram.createSocket({ type: 'udp4', diff --git a/test/src/fs/promises.js b/test/src/fs/promises.js index 0df3ade7aa..291341bc36 100644 --- a/test/src/fs/promises.js +++ b/test/src/fs/promises.js @@ -31,12 +31,12 @@ if (process.platform !== 'ios') { t.equal(access, true, '(X_OK) fixtures/ directory is "executable" - can list items') }) - test('fs.promises.chmod', async (t) => { - const chmod = await fs.chmod(FIXTURES + 'file.txt', 0o777) - t.equal(chmod, undefined, 'file.txt is chmod 777') - }) - if (os.platform() !== 'android') { + test('fs.promises.chmod', async (t) => { + const chmod = await fs.chmod(FIXTURES + 'file.txt', 0o777) + t.equal(chmod, undefined, 'file.txt is chmod 777') + }) + test('fs.promises.mkdir', async (t) => { const dirname = FIXTURES + Math.random().toString(16).slice(2) await fs.mkdir(dirname, {}) From b67f041ea0697228eb62b773e919d7a763fbc1eb Mon Sep 17 00:00:00 2001 From: Joseph Werle Date: Mon, 2 Oct 2023 15:48:49 -0400 Subject: [PATCH 009/256] refactor(src/window/window.hh): store delegate on Window instance for apple --- src/window/window.hh | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/window/window.hh b/src/window/window.hh index 02181d0e64..d270415e48 100644 --- a/src/window/window.hh +++ b/src/window/window.hh @@ -12,7 +12,13 @@ #if defined(__APPLE__) #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR @interface SSCBridgedWebView : WKWebView +@interface SSCWindowDelegate : NSObject +@end #else +@interface SSCWindowDelegate : NSObject +- (void) userContentController: (WKUserContentController*) userContentController + didReceiveScriptMessage: (WKScriptMessage*) scriptMessage; +@end @interface SSCBridgedWebView : WKWebView< WKUIDelegate, NSDraggingDestination, @@ -100,6 +106,8 @@ namespace SSC { NSWindow* window; #endif SSCBridgedWebView* webview; + SSCWindowDelegate* windowDelegate = nullptr; + SSCNavigationDelegate *navigationDelegate = nullptr; #elif defined(__linux__) && !defined(__ANDROID__) GtkSelectionData *selectionData = nullptr; GtkAccelGroup *accelGroup = nullptr; @@ -128,6 +136,9 @@ namespace SSC { #endif Window (App&, WindowOptions); + #if defined(__APPLE__) + ~Window (); + #endif static ScreenSize getScreenSize (); @@ -146,9 +157,9 @@ namespace SSC { void setContextMenu (const String&, const String&); void closeContextMenu (const String&); void closeContextMenu (); -#if defined(__linux__) && !defined(__ANDROID__) + #if defined(__linux__) && !defined(__ANDROID__) void closeContextMenu (GtkWidget *, const String&); -#endif + #endif void setBackgroundColor (int r, int g, int b, float a); void setSystemMenuItemEnabled (bool enabled, int barPos, int menuPos); void setSystemMenu (const String& seq, const String& menu); @@ -424,7 +435,7 @@ namespace SSC { WindowStatus getWindowStatus (int index) { std::lock_guard guard(this->mutex); if (this->destroyed) return WindowStatus::WINDOW_NONE; - if (index >= 0 && inits[index]) { + if (index >= 0 && inits[index] && windows[index] != nullptr) { return windows[index]->status; } From 5b11195471d77ba1e0e9ab3f944bc031dc45ae57 Mon Sep 17 00:00:00 2001 From: Joseph Werle Date: Mon, 2 Oct 2023 15:49:37 -0400 Subject: [PATCH 010/256] fix(src/window/apple.mm): fix system menu configuration, bad state, try/catch setting prefs --- src/window/apple.mm | 235 ++++++++++++++++++++++++++++++------------- src/window/window.hh | 6 +- 2 files changed, 169 insertions(+), 72 deletions(-) diff --git a/src/window/apple.mm b/src/window/apple.mm index ded2243f6f..c529f9ca0d 100644 --- a/src/window/apple.mm +++ b/src/window/apple.mm @@ -27,12 +27,10 @@ - (void) webView: (WKWebView*) webView } @end -#if !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR -@interface SSCWindowDelegate : NSObject -- (void) userContentController: (WKUserContentController*) userContentController - didReceiveScriptMessage: (WKScriptMessage*) scriptMessage; +#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR +@implementation SSCWindowDelegate @end - +#else @implementation SSCWindowDelegate - (void) userContentController: (WKUserContentController*) userContentController didReceiveScriptMessage: (WKScriptMessage*) scriptMessage @@ -668,10 +666,14 @@ - (void) webView: (WKWebView*) webView WKPreferences* prefs = [config preferences]; prefs.javaScriptCanOpenWindowsAutomatically = NO; - if (userConfig["permissions_allow_fullscreen"] == "false") { - [prefs setValue: @NO forKey: @"fullScreenEnabled"]; - } else { - [prefs setValue: @YES forKey: @"fullScreenEnabled"]; + @try { + if (userConfig["permissions_allow_fullscreen"] == "false") { + [prefs setValue: @NO forKey: @"fullScreenEnabled"]; + } else { + [prefs setValue: @YES forKey: @"fullScreenEnabled"]; + } + } @catch (NSException *error) { + debug("Failed to set preference: 'fullScreenEnabled': %@", error); } if (SSC::isDebugEnabled()) { @@ -681,55 +683,95 @@ - (void) webView: (WKWebView*) webView } } - if (userConfig["permissions_allow_clipboard"] == "false") { - [prefs setValue: @NO forKey: @"javaScriptCanAccessClipboard"]; - } else { - [prefs setValue: @YES forKey: @"javaScriptCanAccessClipboard"]; + @try { + if (userConfig["permissions_allow_clipboard"] == "false") { + [prefs setValue: @NO forKey: @"javaScriptCanAccessClipboard"]; + } else { + [prefs setValue: @YES forKey: @"javaScriptCanAccessClipboard"]; + } + } @catch (NSException *error) { + debug("Failed to set preference: 'javaScriptCanAccessClipboard': %@", error); } - if (userConfig["permissions_allow_data_access"] == "false") { - [prefs setValue: @NO forKey: @"storageAPIEnabled"]; - } else { - [prefs setValue: @YES forKey: @"storageAPIEnabled"]; + @try { + if (userConfig["permissions_allow_data_access"] == "false") { + [prefs setValue: @NO forKey: @"storageAPIEnabled"]; + } else { + [prefs setValue: @YES forKey: @"storageAPIEnabled"]; + } + } @catch (NSException *error) { + debug("Failed to set preference: 'storageAPIEnabled': %@", error); } - if (userConfig["permissions_allow_device_orientation"] == "false") { - [prefs setValue: @NO forKey: @"deviceOrientationEventEnabled"]; - } else { - [prefs setValue: @YES forKey: @"deviceOrientationEventEnabled"]; + @try { + if (userConfig["permissions_allow_device_orientation"] == "false") { + [prefs setValue: @NO forKey: @"deviceOrientationEventEnabled"]; + } else { + [prefs setValue: @YES forKey: @"deviceOrientationEventEnabled"]; + } + } @catch (NSException *error) { + debug("Failed to set preference: 'deviceOrientationEventEnabled': %@", error); } if (userConfig["permissions_allow_notifications"] == "false") { - [prefs setValue: @NO forKey: @"appBadgeEnabled"]; - [prefs setValue: @NO forKey: @"notificationsEnabled"]; - [prefs setValue: @NO forKey: @"notificationEventEnabled"]; + @try { + [prefs setValue: @NO forKey: @"appBadgeEnabled"]; + } @catch (NSException *error) { + debug("Failed to set preference: 'deviceOrientationEventEnabled': %@", error); + } + + @try { + [prefs setValue: @NO forKey: @"notificationsEnabled"]; + } @catch (NSException *error) { + debug("Failed to set preference: 'notificationsEnabled': %@", error); + } + + @try { + [prefs setValue: @NO forKey: @"notificationEventEnabled"]; + } @catch (NSException *error) { + debug("Failed to set preference: 'notificationEventEnabled': %@", error); + } } else { - [prefs setValue: @YES forKey: @"appBadgeEnabled"]; - [prefs setValue: @YES forKey: @"notificationsEnabled"]; - [prefs setValue: @YES forKey: @"notificationEventEnabled"]; + @try { + [prefs setValue: @YES forKey: @"appBadgeEnabled"]; + } @catch (NSException *error) { + debug("Failed to set preference: 'appBadgeEnabled': %@", error); + } } #if !TARGET_OS_IPHONE - [prefs setValue: @YES forKey: @"cookieEnabled"]; + @try { + [prefs setValue: @YES forKey: @"cookieEnabled"]; - if (userConfig["permissions_allow_user_media"] == "false") { - [prefs setValue: @NO forKey: @"mediaStreamEnabled"]; - } else { - [prefs setValue: @YES forKey: @"mediaStreamEnabled"]; + if (userConfig["permissions_allow_user_media"] == "false") { + [prefs setValue: @NO forKey: @"mediaStreamEnabled"]; + } else { + [prefs setValue: @YES forKey: @"mediaStreamEnabled"]; + } + } @catch (NSException *error) { + debug("Failed to set preference: 'mediaStreamEnabled': %@", error); } #endif - if (userConfig["permissions_allow_airplay"] == "false") { - config.allowsAirPlayForMediaPlayback = NO; - } else { - config.allowsAirPlayForMediaPlayback = YES; + @try { + if (userConfig["permissions_allow_airplay"] == "false") { + config.allowsAirPlayForMediaPlayback = NO; + } else { + config.allowsAirPlayForMediaPlayback = YES; + } + } @catch (NSException *error) { + debug("%@", error); } config.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; config.websiteDataStore = [WKWebsiteDataStore defaultDataStore]; config.processPool = [WKProcessPool new]; - [prefs setValue: @YES forKey: @"offlineApplicationCacheIsEnabled"]; + @try { + [prefs setValue: @YES forKey: @"offlineApplicationCacheIsEnabled"]; + } @catch (NSException *error) { + debug("Failed to set preference: 'offlineApplicationCacheIsEnabled': %@", error); + } WKUserContentController* controller = [config userContentController]; @@ -792,8 +834,8 @@ - (void) webView: (WKWebView*) webView // bool exiting = false; - SSCWindowDelegate* windowDelegate = [SSCWindowDelegate alloc]; - SSCNavigationDelegate *navigationDelegate = [[SSCNavigationDelegate alloc] init]; + windowDelegate = [SSCWindowDelegate alloc]; + navigationDelegate = [[SSCNavigationDelegate alloc] init]; [controller addScriptMessageHandler: windowDelegate name: @"external"]; // set delegates @@ -832,7 +874,7 @@ - (void) webView: (WKWebView*) webView [=](id self, SEL cmd, WKScriptMessage* scriptMessage) { auto window = (Window*) objc_getAssociatedObject(self, "window"); - if (!scriptMessage) return; + if (!scriptMessage || !window) return; id body = [scriptMessage body]; if (!body || ![body isKindOfClass:[NSString class]]) { return; @@ -893,6 +935,23 @@ - (void) webView: (WKWebView*) webView navigate("0", opts.url); } + Window::~Window () { + this->close(0); + + #if !__has_feature(objc_arc) + if (this->window) { + [this->window release]; + } + + if (this->webview) { + [this->webview release]; + } + #endif + + this->window = nullptr; + this->webview = nullptr; + } + ScreenSize Window::getScreenSize () { NSRect e = [[NSScreen mainScreen] frame]; @@ -912,6 +971,7 @@ - (void) webView: (WKWebView*) webView } void Window::exit (int code) { + this->close(code);; if (onExit != nullptr) onExit(code); } @@ -919,28 +979,46 @@ - (void) webView: (WKWebView*) webView } void Window::close (int code) { - [window performClose:nil]; + if (this->window != nullptr) { + [this->window performClose: nil]; + this->window = nullptr; + } + + if (this->windowDelegate != nullptr) { + objc_removeAssociatedObjects(this->windowDelegate); + this->windowDelegate = nullptr; + } + + if (this->navigationDelegate != nullptr) { + this->navigationDelegate = nullptr; + } } void Window::hide () { - [window orderOut:window]; + if (this->window) { + [this->window orderOut: this->window]; + } this->eval(getEmitToRenderProcessJavaScript("windowHide", "{}")); } void Window::eval (const SSC::String& js) { - [webview evaluateJavaScript: - [NSString stringWithUTF8String:js.c_str()] - completionHandler:nil]; + if (this->webview != nullptr) { + auto string = [NSString stringWithUTF8String:js.c_str()]; + [this->webview evaluateJavaScript: string completionHandler: nil]; + } } void Window::setSystemMenuItemEnabled (bool enabled, int barPos, int menuPos) { + if (!this->window || !this->webview) return; NSMenu* menuBar = [NSApp mainMenu]; NSArray* menuBarItems = [menuBar itemArray]; + if (menuBarItems.count == 0) return; - NSMenu* menu = menuBarItems[barPos]; + NSMenu* menu = [menuBarItems[barPos] submenu]; if (!menu) return; NSArray* menuItems = [menu itemArray]; + if (menuItems.count == 0) return; NSMenuItem* menuItem = menuItems[menuPos]; if (!menuItem) return; @@ -952,15 +1030,15 @@ - (void) webView: (WKWebView*) webView void Window::navigate (const SSC::String& seq, const SSC::String& value) { auto url = [NSURL URLWithString: [NSString stringWithUTF8String: value.c_str()]]; - if (url != nullptr) { + if (url != nullptr && this->webview != nullptr) { if (String(url.scheme.UTF8String) == "file") { NSString* allowed = [[NSBundle mainBundle] resourcePath]; - [webview loadFileURL: url + [this->webview loadFileURL: url allowingReadAccessToURL: [NSURL fileURLWithPath: allowed] ]; } else { auto request = [NSMutableURLRequest requestWithURL: url]; - [webview loadRequest: request]; + [this->webview loadRequest: request]; } if (seq.size() > 0) { @@ -971,15 +1049,22 @@ - (void) webView: (WKWebView*) webView } SSC::String Window::getTitle () { - return SSC::String([this->window.title UTF8String]); + if (this->window) { + return SSC::String([this->window.title UTF8String]); + } + + return ""; } void Window::setTitle (const SSC::String& value) { - [window setTitle:[NSString stringWithUTF8String:value.c_str()]]; + if (this->window) { + auto title = [NSString stringWithUTF8String:value.c_str()]; + [this->window setTitle: title]; + } } ScreenSize Window::getSize () { - if (this->window == nil) { + if (this->window == nullptr) { return ScreenSize {0, 0}; } @@ -995,14 +1080,18 @@ - (void) webView: (WKWebView*) webView } void Window::setSize (int width, int height, int hints) { - [window setFrame: NSMakeRect(0.f, 0.f, (float) width, (float) height) - display: YES - animate: NO]; + if (this->window) { + [this->window + setFrame: NSMakeRect(0.f, 0.f, (float) width, (float) height) + display: YES + animate: NO + ]; - [window center]; + [this->window center]; - this->height = height; - this->width = width; + this->height = height; + this->width = width; + } } int Window::openExternal (const SSC::String& s) { @@ -1019,21 +1108,25 @@ - (void) webView: (WKWebView*) webView } void Window::showInspector () { - // This is a private method on the webview, so we need to use - // the pragma keyword to suppress the access warning. - #pragma clang diagnostic ignored "-Wobjc-method-access" - [[this->webview _inspector] show]; + if (this->webview) { + // This is a private method on the webview, so we need to use + // the pragma keyword to suppress the access warning. + #pragma clang diagnostic ignored "-Wobjc-method-access" + [[this->webview _inspector] show]; + } } void Window::setBackgroundColor (int r, int g, int b, float a) { - CGFloat sRGBComponents[4] = { r / 255.0, g / 255.0, b / 255.0, a }; - NSColorSpace *colorSpace = [NSColorSpace sRGBColorSpace]; - - [window setBackgroundColor: - [NSColor colorWithColorSpace: colorSpace - components: sRGBComponents - count: 4] - ]; + if (this->window) { + CGFloat sRGBComponents[4] = { r / 255.0, g / 255.0, b / 255.0, a }; + NSColorSpace *colorSpace = [NSColorSpace sRGBColorSpace]; + + [this->window setBackgroundColor: + [NSColor colorWithColorSpace: colorSpace + components: sRGBComponents + count: 4] + ]; + } } void Window::setContextMenu (const SSC::String& seq, const SSC::String& value) { @@ -1099,7 +1192,7 @@ - (void) webView: (WKWebView*) webView mainMenu = [[NSMenu alloc] init]; // Create the main menu bar - [NSApp setMainMenu:mainMenu]; + [NSApp setMainMenu: mainMenu]; [mainMenu release]; mainMenu = nil; diff --git a/src/window/window.hh b/src/window/window.hh index d270415e48..e8d01ddb4c 100644 --- a/src/window/window.hh +++ b/src/window/window.hh @@ -11,7 +11,6 @@ #if defined(__APPLE__) #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR -@interface SSCBridgedWebView : WKWebView @interface SSCWindowDelegate : NSObject @end #else @@ -19,6 +18,11 @@ - (void) userContentController: (WKUserContentController*) userContentController didReceiveScriptMessage: (WKScriptMessage*) scriptMessage; @end +#endif + +#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR +@interface SSCBridgedWebView : WKWebView +#else @interface SSCBridgedWebView : WKWebView< WKUIDelegate, NSDraggingDestination, From 93ae8719594723a9d098859ea1aafc23306e72e3 Mon Sep 17 00:00:00 2001 From: Joseph Werle Date: Mon, 2 Oct 2023 21:11:30 -0400 Subject: [PATCH 011/256] feat(api/internal/permissions.js): internal and polyfilled permissions API --- api/internal/permissions.js | 134 ++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 api/internal/permissions.js diff --git a/api/internal/permissions.js b/api/internal/permissions.js new file mode 100644 index 0000000000..60f8129762 --- /dev/null +++ b/api/internal/permissions.js @@ -0,0 +1,134 @@ +/* global EventTarget, Event */ +import { IllegalConstructorError } from '../errors.js' +import ipc from '../ipc.js' +import os from '../os.js' + +const isAndroid = os.platform() === 'android' + +class PermissionStatus extends EventTarget { + #onchange = null + #state = null + #name = null + + // eslint-disable-next-line + [Symbol.toStringTag] = 'PermissionStatus' + + constructor (name, subscribe) { + super() + this.#name = name + subscribe((state) => { + if (this.#state !== state) { + this.#state = state + this.dispatchEvent(new Event('change')) + } + }) + } + + get name () { + return this.#name + } + + get state () { + return this.#state + } + + set onchange (onchange) { + if (typeof this.#onchange === 'function') { + this.removeEventListener('change', this.#onchange) + } + + if (typeof onchange === 'function') { + this.#onchange = onchange + this.addEventListener('change', onchange) + } + } + + get onchange () { + return this.#onchange + } +} + +/** + * @ignore + * @param {string} name} + * @return {function} + */ +function getPlatformFunction (name) { + if (!globalThis.window?.navigator?.permissions?.[name]) return null + const value = globalThis.window.navigator.permissions[name] + return value.bind(globalThis.navigator.permissions) +} + +const platform = { + query: getPlatformFunction('query') +} + +/** + * @param {{ name: string }} descriptor + * @return {Promise} + */ +export async function query (descriptor) { + if (!isAndroid) { + return platform.query(descriptor) + } + + const types = [ + 'geolocation', + 'notifications', + 'push', + 'persistent-storage', + 'midi', + 'storage-access' + ] + + if (arguments.length === 0) { + throw new TypeError( + 'Failed to execute \'query\' on \'Permissions\': ' + + '1 argument required, but only 0 present.' + ) + } + + if (!descriptor || typeof descriptor !== 'object') { + throw new TypeError( + 'Failed to execute \'query\' on \'Permissions\': ' + + 'parameter 1 is not of type \'object\'.' + ) + } + + const { name } = descriptor + + if (name === undefined) { + throw new TypeError( + 'Failed to execute \'query\' on \'Permissions\': ' + + 'Failed to read the \'name\' property from \'PermissionDescriptor\': ' + + 'Required member is undefined.' + ) + } + + if (typeof name !== 'string' || name.length === 0 || !types.includes(name)) { + throw new TypeError( + 'Failed to execute \'query\' on \'Permissions\': ' + + 'Failed to read the \'name\' property from \'PermissionDescriptor\': ' + + `The provided value '${name}' is not a valid enum value of type PermissionName.` + ) + } + + const result = await ipc.send('permissions.query', { name }) + const status = new PermissionStatus(name, async (update) => { + queueMicrotask(() => update(result.data.state)) + }) + + if (result.err) { + throw result.err + } + + return status +} + +class Permissions { + constructor () { + throw new IllegalConstructorError() + } +} + +export default Object.assign(Object.create(Permissions.prototype), { query }) From 2929948c79bcd609cfe5654a62103edcde42d3ae Mon Sep 17 00:00:00 2001 From: Joseph Werle Date: Mon, 2 Oct 2023 21:11:48 -0400 Subject: [PATCH 012/256] refactor(api/internal/monkeypatch.js): patch 'navigator.permissions' --- api/internal/monkeypatch.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/api/internal/monkeypatch.js b/api/internal/monkeypatch.js index fdd5910aae..b7a57fb0b0 100644 --- a/api/internal/monkeypatch.js +++ b/api/internal/monkeypatch.js @@ -2,6 +2,7 @@ import { fetch, Headers, Request, Response } from '../fetch.js' import { URL, URLPattern, URLSearchParams } from '../url.js' import geolocation from './geolocation.js' +import permissions from './permissions.js' import ipc from '../ipc.js' @@ -30,7 +31,13 @@ export function init () { Response }) - Object.assign(globalThis.navigator?.geolocation ?? {}, geolocation) + try { + globalThis.navigator.geolocation = Object.assign(globalThis.navigator?.geolocation ?? {}, geolocation) + } catch {} + + try { + globalThis.navigator.permissions = Object.assign(globalThis.navigator?.permissions ?? {}, permissions) + } catch {} applied = true // create tag in document if it doesn't exist @@ -65,6 +72,4 @@ export function init () { } } -export default { - init -} +export default init() From 6dc1439d8c55eaa6596c9cda0765bbdb6f4ccc88 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Mon, 2 Oct 2023 21:12:16 -0400 Subject: [PATCH 013/256] refactor(api/internal/geolocation.js): request permissions prompt on android --- api/internal/geolocation.js | 113 +++++++++++++++++++++++++++++++++--- 1 file changed, 105 insertions(+), 8 deletions(-) diff --git a/api/internal/geolocation.js b/api/internal/geolocation.js index dd03835ae5..a757802519 100644 --- a/api/internal/geolocation.js +++ b/api/internal/geolocation.js @@ -1,13 +1,30 @@ /* global EventTarget, CustomEvent, GeolocationCoordinates, GeolocationPosition, GeolocationPositionError */ +import hooks from 'socket:hooks' import ipc from '../ipc.js' import os from '../os.js' +const isAndroid = os.platform() === 'android' const isApple = os.platform() === 'darwin' + const watchers = {} let currentWatchIdenfifier = 0 +/** + * @ignore + */ class Watcher extends EventTarget { + /** + * @type {number} + */ + identifier = 0 + + /** + * @type {function(CustomEvent): undefined} + */ + // eslint-disable-next-line + listener = (event) => void event + constructor (identifier) { super() @@ -27,6 +44,9 @@ class Watcher extends EventTarget { } } +/** + * @ignore + */ function createGeolocationPosition (data) { const coords = Object.create(GeolocationCoordinates.prototype, { latitude: { @@ -84,24 +104,70 @@ function createGeolocationPosition (data) { }) } +/** + * @ignore + * @param {string} name} + * @return {function} + */ +function getPlatformFunction (name) { + if (!globalThis.window?.navigator?.geolocation?.[name]) return null + const value = globalThis.window.navigator.geolocation[name] + return value.bind(globalThis.navigator.geolocation) +} + +/** + * @ignore + */ export const platform = { - getCurrentPosition: globalThis.navigator?.getCurrentPosition?.bind(globalThis.navigator), - watchPosition: globalThis.navigator?.watchPosition?.bind(globalThis.navigator), - clearWatch: globalThis.navigator?.clearWatch?.bind(globalThis.navigator) + getCurrentPosition: getPlatformFunction('getCurrentPosition'), + watchPosition: getPlatformFunction('watchPosition'), + clearWatch: getPlatformFunction('clearWatch') } -export async function getCurrentPosition (onSuccess, onError, options = {}) { +/** + * @param {function(GeolocationPosition)} onSuccess + * @param {onError(Error)} onError + * @param {?({ timeout?: number })} [options] + * @return {Promise} + */ +export async function getCurrentPosition ( + onSuccess, + onError, + options = { timeout: null } +) { + if (isAndroid) { + await new Promise((resolve) => hooks.onReady(resolve)) + + const result = await ipc.send('permissions.request', { name: 'geolocation' }) + + if (result.err) { + if (typeof onError === 'function') { + onError(result.err) + return + } else { + throw result.err + } + } + } + if (!isApple) { return platform.getCurrentPosition(...arguments) } - if (typeof onSuccess !== 'function') { + if (arguments.length === 0) { throw new TypeError( 'Failed to execute \'getCurrentPosition\' on \'Geolocation\': ' + '1 argument required, but only 0 present.' ) } + if (typeof onSuccess !== 'function') { + throw new TypeError( + 'Failed to execute \'getCurrentPosition\' on \'Geolocation\': ' + + 'parameter 1 is not of type \'function\'.' + ) + } + let timer = null let didTimeout = false if (Number.isFinite(options?.timeout)) { @@ -143,18 +209,48 @@ export async function getCurrentPosition (onSuccess, onError, options = {}) { } } -export function watchPosition (onSuccess, onError, options = {}) { +/** + * @param {function(GeolocationPosition)} onSuccess + * @param {function(Error)} onError + * @param {?({ timeout?: number })} [options] + * @return {Promise} + */ +export function watchPosition ( + onSuccess, + onError, + options = { timeout: null } +) { + if (isAndroid) { + const result = ipc.sendSync('permissions.request', { name: 'geolocation' }) + + if (result.err) { + if (typeof onError === 'function') { + onError(result.err) + return + } else { + throw result.err + } + } + } + if (!isApple) { return platform.watchPosition(...arguments) } - if (typeof onSuccess !== 'function') { + if (arguments.length === 0) { throw new TypeError( 'Failed to execute \'getCurrentPosition\' on \'Geolocation\': ' + '1 argument required, but only 0 present.' ) } + if (typeof onSuccess !== 'function') { + throw new TypeError( + 'Failed to execute \'getCurrentPosition\' on \'Geolocation\': ' + + 'parameter 1 is not of type \'function\'.' + ) + } + const identifier = currentWatchIdenfifier + 1 let timer = null @@ -192,8 +288,9 @@ export function watchPosition (onSuccess, onError, options = {}) { const watcher = new Watcher(identifier) watchers[identifier] = watcher watcher.addEventListener('position', (event) => { + const detail = /** @type {CustomEvent} */ (event) clearTimeout(timer) - onSuccess(event.detail) + onSuccess(/** @type {GeolocationPosition} */ (detail)) }) }) From 114e1431e802bf195624dd58cd0975fa81133c87 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Mon, 2 Oct 2023 21:12:31 -0400 Subject: [PATCH 014/256] refactor(src/core/runtime-preload.hh): do not wait for 'interactive' DOM on android --- src/core/runtime-preload.hh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/runtime-preload.hh b/src/core/runtime-preload.hh index 3b8b7d2fdb..805d1b872e 100644 --- a/src/core/runtime-preload.hh +++ b/src/core/runtime-preload.hh @@ -131,11 +131,15 @@ namespace SSC { ); preload += ( + #if !defined(__linux__) && !defined(__ANDROID__) "document.addEventListener('readystatechange', () => { \n" " if (document.readyState === 'interactive') { \n" + #endif " import('socket:internal/init').catch(console.error); \n" + #if !defined(__linux__) && !defined(__ANDROID__) " } \n" "}, { once: true }); \n" + #endif ); return preload; From eed263b0ca28fe53ca3fccf07aa8f322c40bf876 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Mon, 2 Oct 2023 21:12:45 -0400 Subject: [PATCH 015/256] refactor(api/internal/init.js): import 'monkeypatch.js' first --- api/internal/init.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/api/internal/init.js b/api/internal/init.js index 637b3e4d4e..9cafecb99f 100644 --- a/api/internal/init.js +++ b/api/internal/init.js @@ -12,17 +12,16 @@ console.assert( 'This could lead to undefined behavior.' ) +import './monkeypatch.js' + import { IllegalConstructor, InvertedPromise } from '../util.js' import { Event, CustomEvent, ErrorEvent } from '../events.js' -import monkeypatch from './monkeypatch.js' import location from '../location.js' import { URL } from '../url.js' const RUNTIME_INIT_EVENT_NAME = '__runtime_init__' const GlobalWorker = globalThis.Worker || class Worker extends EventTarget {} -monkeypatch.init() - // only patch a webview or worker context if ((globalThis.window || globalThis.self) === globalThis) { if (typeof globalThis.queueMicrotask === 'function') { From 9a4e101f745f7ad73ea435392fea0149104610fa Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Mon, 2 Oct 2023 21:13:33 -0400 Subject: [PATCH 016/256] refactor(src/cli/cli.cc): handle 'allow_notifications' for Android --- src/cli/cli.cc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index bc4e4a2af4..86a56dc719 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -2952,9 +2952,16 @@ int main (const int argc, const char* argv[]) { manifestContext["android_manifest_xml_permissions"] = ""; + if (settings["permission_allow_notifications"] != "false") { + manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\" />\n"; + } + if (settings["permission_allow_geolocation"] != "false") { manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />\n"; manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" />\n"; + if (settings["permission_allow_geolocation_in_background"] != "false") { + manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.ACCESS_BACKGROUND_LOCATION\" />\n"; + } } if (settings["permission_allow_user_media"] != "false") { From 3c94f227c055443a74320748ae80083e55407ec7 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Mon, 2 Oct 2023 21:14:18 -0400 Subject: [PATCH 017/256] refactor(src/android/main.kt): introduce 'requestPermissions' API --- src/android/main.kt | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/android/main.kt b/src/android/main.kt index 909226b00f..219ac44d9b 100644 --- a/src/android/main.kt +++ b/src/android/main.kt @@ -19,6 +19,11 @@ object console { } } +class PermissionRequest (callback: (Boolean) -> Unit) { + val id: Int = (0..16384).random().toInt() + val callback = callback +} + /** * An entry point for the main activity specified in * `AndroidManifest.xml` and which can be overloaded in `socket.ini` for @@ -32,6 +37,8 @@ open class MainActivity : WebViewActivity() { open lateinit var runtime: Runtime open lateinit var window: Window + val permissionRequests = mutableListOf<PermissionRequest>() + companion object { init { System.loadLibrary("socket-runtime") @@ -43,6 +50,29 @@ open class MainActivity : WebViewActivity() { ?: "/sdcard/Android/data/__BUNDLE_IDENTIFIER__/files" } + fun checkPermission (permission: String): Boolean { + val status = androidx.core.content.ContextCompat.checkSelfPermission( + this.applicationContext, + permission + ) + + if (status == android.content.pm.PackageManager.PERMISSION_GRANTED) { + return true + } + + return false + } + + fun requestPermissions (permissions: Array<String>, callback: (Boolean) -> Unit) { + val request = PermissionRequest(callback) + this.permissionRequests.add(request) + androidx.core.app.ActivityCompat.requestPermissions( + this, + permissions, + request.id + ) + } + override fun onCreate (state: android.os.Bundle?) { // called before `super.onCreate()` this.supportActionBar?.hide() @@ -114,4 +144,18 @@ open class MainActivity : WebViewActivity() { ): Boolean { return this.window.onSchemeRequest(request, response, stream) } + + override fun onRequestPermissionsResult ( + requestCode: Int, + permissions: Array<String>, + grantResults: IntArray + ) { + for (request in this.permissionRequests) { + if (request.id == requestCode) { + this.permissionRequests.remove(request) + request.callback(grantResults.all { r -> r == android.content.pm.PackageManager.PERMISSION_GRANTED }) + break + } + } + } } From 567908334ca8a0e1c224718580ea0fd1dbf20bf7 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Mon, 2 Oct 2023 21:14:34 -0400 Subject: [PATCH 018/256] refactor(src/android/runtime.cc): normalize permission name --- src/android/runtime.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/android/runtime.cc b/src/android/runtime.cc index fa842274f1..a278869a7a 100644 --- a/src/android/runtime.cc +++ b/src/android/runtime.cc @@ -14,7 +14,7 @@ namespace SSC::android { bool Runtime::isPermissionAllowed (const String& name) const { static const auto config = SSC::getUserConfig(); - const auto permission = String("permissions_allow_") + name; + const auto permission = String("permissions_allow_") + replace(name, "-", "_"); // `true` by default if (!config.contains(permission)) { From 21e5be7b3ab1f76820ff31bfa12cba4d20313446 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Mon, 2 Oct 2023 21:15:59 -0400 Subject: [PATCH 019/256] refactor(src/android/webview.kt): chrome client - handle geolocation permission prompt callback - handle more permission requests --- src/android/webview.kt | 51 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/src/android/webview.kt b/src/android/webview.kt index 2d408a7182..bb93f2537d 100644 --- a/src/android/webview.kt +++ b/src/android/webview.kt @@ -25,13 +25,60 @@ fun isAssetUri (uri: android.net.Uri): Boolean { } /** - * @TODO * @see https://developer.android.com/reference/kotlin/android/webkit/WebView */ open class WebView (context: android.content.Context) : android.webkit.WebView(context) /** - * @TODO + * @see https://developer.android.com/reference/kotlin/android/webkit/WebViewClient + */ +open class WebChromeClient (activity: MainActivity) : android.webkit.WebChromeClient() { + protected val activity = WeakReference(activity) + + override fun onGeolocationPermissionsShowPrompt ( + origin: String, + callback: android.webkit.GeolocationPermissions.Callback + ) { + val runtime = this.activity.get()?.runtime ?: return callback(origin, false, false) + val allowed = runtime.isPermissionAllowed("geolocation") + + callback(origin, allowed, allowed) + } + + override fun onPermissionRequest (request: android.webkit.PermissionRequest) { + val runtime = this.activity.get()?.runtime ?: return request.deny() + val resources = request.resources + var grants = mutableListOf<String>() + for (resource in resources) { + when (resource) { + android.webkit.PermissionRequest.RESOURCE_AUDIO_CAPTURE -> { + if (runtime.isPermissionAllowed("microphone") || runtime.isPermissionAllowed("user_media")) { + grants.add(android.webkit.PermissionRequest.RESOURCE_AUDIO_CAPTURE) + } + } + + android.webkit.PermissionRequest.RESOURCE_VIDEO_CAPTURE -> { + if (runtime.isPermissionAllowed("camera") || runtime.isPermissionAllowed("user_media")) { + grants.add(android.webkit.PermissionRequest.RESOURCE_VIDEO_CAPTURE) + } + } + + // auto grant EME + android.webkit.PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID -> { + grants.add(android.webkit.PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID) + } + } + } + + if (grants.size > 0) { + request.grant(grants.toTypedArray()) + } else { + request.deny() + } + } +} + +/** * @see https://developer.android.com/reference/kotlin/android/webkit/WebViewClient */ open class WebViewClient (activity: WebViewActivity) : android.webkit.WebViewClient() { From 61ca057dd1dc71a4bb50e5a04f4bcf507fc49cf6 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Mon, 2 Oct 2023 21:16:56 -0400 Subject: [PATCH 020/256] refactor(src/android/window.kt): use chrome client --- src/android/window.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/android/window.kt b/src/android/window.kt index cbb8e1029c..7fbd3ba086 100644 --- a/src/android/window.kt +++ b/src/android/window.kt @@ -63,7 +63,10 @@ open class Window (runtime: Runtime, activity: MainActivity) { settings.mixedContentMode = android.webkit.WebSettings.MIXED_CONTENT_ALWAYS_ALLOW activity.client.putRootDirectory(rootDirectory) + + // clients webViewClient = activity.client + webChromeClient = WebChromeClient(activity) addJavascriptInterface(userMessageHandler, "external") loadUrl("https://__BUNDLE_IDENTIFIER__$filename") From 3637fc7e6ad69967b5f9ee18955cb8c5dda0f262 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Mon, 2 Oct 2023 21:17:31 -0400 Subject: [PATCH 021/256] feat(src/android/bridge.kt): permissions.* IPC API - introduce 'permissions.query' IPC API on Android to query the current permission state - introduce 'permissions.request' IPC API on Android to request a permissions prompt --- src/android/bridge.kt | 124 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/src/android/bridge.kt b/src/android/bridge.kt index ca22e37bfd..76bab65d85 100644 --- a/src/android/bridge.kt +++ b/src/android/bridge.kt @@ -151,7 +151,10 @@ open class Bridge (runtime: Runtime, configuration: IBridgeConfiguration) { bytes: ByteArray? = null, callback: RouteCallback ): Boolean { + val activity = this.runtime.get()?.activity?.get() ?: return false + val runtime = activity.runtime val message = Message(value) + message.bytes = bytes if (buffers.contains(message.seq)) { @@ -160,6 +163,122 @@ open class Bridge (runtime: Runtime, configuration: IBridgeConfiguration) { } when (message.command) { + "permissions.request" -> { + val name = message.get("name") ?: return false + val permissions = mutableListOf<String>() + + when (name) { + "geolocation" -> { + if ( + activity.checkPermission("android.permission.ACCESS_COARSE_LOCATION") && + activity.checkPermission("android.permission.ACCESS_FINE_LOCATION") + ) { + callback(Result(0, message.seq, message.command, "{}")) + return true + } + + permissions.add("android.permission.ACCESS_COARSE_LOCATION") + permissions.add("android.permission.ACCESS_FINE_LOCATION") + } + + "push", "notifications" -> { + if (activity.checkPermission("android.permission.POST_NOTIFICATIONS")) { + callback(Result(0, message.seq, message.command, "{}")) + return true + } + + permissions.add("android.permission.POST_NOTIFICATIONS") + } + + else -> { + callback(Result(0, message.seq, message.command, """{ + "err": { + "message": "Unknown permission requested: '$name'" + } + }""")) + return true + } + } + + activity.requestPermissions(permissions.toTypedArray(), fun (granted: Boolean) { + if (granted) { + callback(Result(0, message.seq, message.command, "{}")) + } else { + callback(Result(0, message.seq, message.command, """{ + "err": { + "message": "User denied permission request for '$name'" + } + }""")) + } + }) + + return true + } + + "permissions.query" -> { + val name = message.get("name") ?: return false + if (name == "geolocation") { + if (!runtime.isPermissionAllowed("geolocation")) { + callback(Result(0, message.seq, message.command, """{ + "err": { + "message": "User denied permissions to access the device's location" + } + }""")) + } else if ( + activity.checkPermission("android.permission.ACCESS_COARSE_LOCATION") && + activity.checkPermission("android.permission.ACCESS_FINE_LOCATION") + ) { + callback(Result(0, message.seq, message.command, """{ + "data": { + "state": "granted" + } + }""")) + } else { + callback(Result(0, message.seq, message.command, """{ + "data": { + "state": "prompt" + } + }""")) + } + } + + if (name == "notifications" || name == "push") { + if (!runtime.isPermissionAllowed("notifications")) { + callback(Result(0, message.seq, message.command, """{ + "err": { + "message": "User denied permissions to show notifications" + } + }""")) + } else if ( + activity.checkPermission("android.permission.POST_NOTIFICATIONS") + ) { + callback(Result(0, message.seq, message.command, """{ + "data": { + "state": "granted" + } + }""")) + } else { + callback(Result(0, message.seq, message.command, """{ + "data": { + "state": "prompt" + } + }""")) + } + } + + if (name == "persistent-storage" || name == "storage-access") { + if (!runtime.isPermissionAllowed("data_access")) { + callback(Result(0, message.seq, message.command, """{ + "err": { + "message": "User denied permissions for ${name.replace('-', ' ')}" + } + }""")) + } + } + + return true + } + "buffer.map" -> { if (bytes != null) { buffers[message.seq] = bytes @@ -210,6 +329,11 @@ open class Bridge (runtime: Runtime, configuration: IBridgeConfiguration) { var dest = message.get("dest") message.set("dest", root.resolve(java.nio.file.Paths.get(dest)).toString()) } + + if (message.has("dst")) { + var dest = message.get("dst") + message.set("dst", root.resolve(java.nio.file.Paths.get(dest)).toString()) + } } val request = RouteRequest(this.nextRequestId++, callback) From d754d847a45d50cad409bb1dd98cbcd52020a6ec Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Mon, 2 Oct 2023 21:19:24 -0400 Subject: [PATCH 022/256] chore(api/index.d.ts): generate types --- api/index.d.ts | 49 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/api/index.d.ts b/api/index.d.ts index 73edbb8b4c..2b2841a285 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -6069,13 +6069,29 @@ declare module "socket:stream-relay" { import def from "socket:stream-relay/index"; } declare module "socket:internal/geolocation" { - export function getCurrentPosition(onSuccess: any, onError: any, options?: {}, ...args: any[]): Promise<any>; - export function watchPosition(onSuccess: any, onError: any, options?: {}, ...args: any[]): any; + /** + * @param {function(GeolocationPosition)} onSuccess + * @param {onError(Error)} onError + * @param {?({ timeout?: number })} [options] + * @return {Promise} + */ + export function getCurrentPosition(onSuccess: (arg0: GeolocationPosition) => any, onError: any, options?: ({ + timeout?: number; + }) | null, ...args: any[]): Promise<any>; + /** + * @param {function(GeolocationPosition)} onSuccess + * @param {function(Error)} onError + * @param {?({ timeout?: number })} [options] + * @return {Promise} + */ + export function watchPosition(onSuccess: (arg0: GeolocationPosition) => any, onError: (arg0: Error) => any, options?: ({ + timeout?: number; + }) | null, ...args: any[]): Promise<any>; export function clearWatch(id: any, ...args: any[]): any; export namespace platform { - let getCurrentPosition: any; - let watchPosition: any; - let clearWatch: any; + let getCurrentPosition: Function; + let watchPosition: Function; + let clearWatch: Function; } namespace _default { export { getCurrentPosition }; @@ -6102,11 +6118,28 @@ declare module "socket:internal/globals" { get(name: any): any; }; } +declare module "socket:internal/permissions" { + /** + * @param {{ name: string }} descriptor + * @return {Promise<PermissionStatus>} + */ + export function query(descriptor: { + name: string; + }, ...args: any[]): Promise<PermissionStatus>; + const _default: any; + export default _default; + class PermissionStatus extends EventTarget { + constructor(name: any, subscribe: any); + get name(): string; + get state(): any; + set onchange(arg: any); + get onchange(): any; + #private; + } +} declare module "socket:internal/monkeypatch" { export function init(): void; - namespace _default { - export { init }; - } + const _default: void; export default _default; } declare module "socket:internal/init" { From bcb16f44b8783f15dc926da8c34c7795ff999fc3 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 3 Oct 2023 13:26:59 -0400 Subject: [PATCH 023/256] refavtor(src/core/preload.{cc,hh}): move preload to own unit --- src/core/core.hh | 2 +- src/core/{runtime-preload.hh => preload.cc} | 42 +++++++++++---------- src/core/preload.hh | 20 ++++++++++ 3 files changed, 44 insertions(+), 20 deletions(-) rename src/core/{runtime-preload.hh => preload.cc} (79%) create mode 100644 src/core/preload.hh diff --git a/src/core/core.hh b/src/core/core.hh index 30cdc36ba4..85492743aa 100644 --- a/src/core/core.hh +++ b/src/core/core.hh @@ -41,7 +41,7 @@ #endif #include "json.hh" -#include "runtime-preload.hh" +#include "preload.hh" #if defined(__APPLE__) @interface SSCBluetoothController : NSObject< diff --git a/src/core/runtime-preload.hh b/src/core/preload.cc similarity index 79% rename from src/core/runtime-preload.hh rename to src/core/preload.cc index 805d1b872e..a6a711cdf7 100644 --- a/src/core/runtime-preload.hh +++ b/src/core/preload.cc @@ -1,14 +1,7 @@ -#ifndef RUNTIME_PRELOAD_HH -#define RUNTIME_PRELOAD_HH - -#include "../window/options.hh" +#include "preload.hh" namespace SSC { - struct PreloadOptions { - bool module = false; - }; - - inline String createPreload ( + String createPreload ( const WindowOptions opts, const PreloadOptions preloadOptions ) { @@ -127,26 +120,37 @@ namespace SSC { " Object.freeze(globalThis.__args.config); \n" " Object.freeze(globalThis.__args.argv); \n" " Object.freeze(globalThis.__args.env); \n" + " \n" + " try { \n" + " const event = '__runtime_init__'; \n" + " let onload = null \n" + " Object.defineProperty(globalThis, 'onload', { \n" + " get: () => onload, \n" + " set (value) { \n" + " const opts = { once: true }; \n" + " if (onload) { \n" + " globalThis.removeEventListener(event, onload, opts); \n" + " onload = null; \n" + " } \n" + " \n" + " if (typeof value === 'function') { \n" + " onload = value; \n" + " globalThis.addEventListener(event, onload, opts); \n" + " } \n" + " } \n" + " }); \n" + " } catch {} \n" "})(); \n" ); preload += ( - #if !defined(__linux__) && !defined(__ANDROID__) "document.addEventListener('readystatechange', () => { \n" - " if (document.readyState === 'interactive') { \n" - #endif + " if (/interactive|complete/.test(document.readyState)) { \n" " import('socket:internal/init').catch(console.error); \n" - #if !defined(__linux__) && !defined(__ANDROID__) " } \n" "}, { once: true }); \n" - #endif ); return preload; } - - inline SSC::String createPreload (WindowOptions opts) { - return createPreload(opts, PreloadOptions {}); - } } -#endif diff --git a/src/core/preload.hh b/src/core/preload.hh new file mode 100644 index 0000000000..871408ef63 --- /dev/null +++ b/src/core/preload.hh @@ -0,0 +1,20 @@ +#ifndef CORE_PRELOAD_HH +#define CORE_PRELOAD_HH + +#include "../window/options.hh" + +namespace SSC { + struct PreloadOptions { + bool module = false; + }; + + String createPreload ( + const WindowOptions opts, + const PreloadOptions preloadOptions + ); + + inline SSC::String createPreload (WindowOptions opts) { + return createPreload(opts, PreloadOptions {}); + } +} +#endif From 0822322db3045e1570351c2c2433e8e72cee7298 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 3 Oct 2023 13:27:21 -0400 Subject: [PATCH 024/256] chore(bin/generate-gradle-files.sh): bump gradle --- bin/generate-gradle-files.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/generate-gradle-files.sh b/bin/generate-gradle-files.sh index a5441e5589..f069d97824 100755 --- a/bin/generate-gradle-files.sh +++ b/bin/generate-gradle-files.sh @@ -17,7 +17,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' + classpath 'com.android.tools.build:gradle:7.4.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:\$kotlin_version" } } From c55525929dc586719830b7f368656a310573914b Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 3 Oct 2023 13:31:49 -0400 Subject: [PATCH 025/256] refactor(src/cli/cli.cc): include 'preload.{cc,hh}' in android build --- src/cli/cli.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 86a56dc719..4bd2225ffc 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -2847,7 +2847,8 @@ int main (const int argc, const char* argv[]) { fs::copy(trim(prefixFile("src/android/window.cc")), jni / "android", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/core/core.hh")), jni / "core", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/core/json.hh")), jni / "core", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/core/runtime-preload.hh")), jni / "core", fs::copy_options::overwrite_existing); + fs::copy(trim(prefixFile("src/core/preload.hh")), jni / "core", fs::copy_options::overwrite_existing); + fs::copy(trim(prefixFile("src/core/preload.cc")), jni / "core", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/ipc/ipc.hh")), jni / "ipc", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/window/options.hh")), jni / "window", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/window/window.hh")), jni / "window", fs::copy_options::overwrite_existing); From d7e94c83943d781dc050956dc195474246d0d558 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 3 Oct 2023 13:35:18 -0400 Subject: [PATCH 026/256] refactor(src/android): inject preload as early as possible --- src/android/main.kt | 8 ++++++++ src/android/webview.kt | 22 ++++++++++++++++++++++ src/android/window.kt | 35 ++++++++++++++++++++++++++++++----- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/src/android/main.kt b/src/android/main.kt index 219ac44d9b..f47e554d04 100644 --- a/src/android/main.kt +++ b/src/android/main.kt @@ -137,6 +137,14 @@ open class MainActivity : WebViewActivity() { this.window.onPageStarted(view, url, bitmap) } + override fun onPageFinished ( + view: android.webkit.WebView, + url: String + ) { + super.onPageFinished(view, url) + this.window.onPageFinished(view, url) + } + override fun onSchemeRequest ( request: android.webkit.WebResourceRequest, response: android.webkit.WebResourceResponse, diff --git a/src/android/webview.kt b/src/android/webview.kt index bb93f2537d..6986716495 100644 --- a/src/android/webview.kt +++ b/src/android/webview.kt @@ -76,6 +76,14 @@ open class WebChromeClient (activity: MainActivity) : android.webkit.WebChromeCl request.deny() } } + + override fun onProgressChanged ( + webview: android.webkit.WebView, + progress: Int + ) { + val activity = this.activity.get() ?: return; + activity.window.onProgressChanged(webview, progress) + } } /** @@ -348,6 +356,13 @@ export default module ) { this.activity.get()?.onPageStarted(view, url, bitmap) } + + override fun onPageFinished ( + view: android.webkit.WebView, + url: String + ) { + this.activity.get()?.onPageFinished(view, url) + } } /** @@ -391,6 +406,13 @@ open class WebViewActivity : androidx.appcompat.app.AppCompatActivity() { console.log("WebViewActivity is loading: $url") } + open fun onPageFinished ( + view: android.webkit.WebView, + url: String + ) { + console.log("WebViewActivity finished loading: $url") + } + open fun onSchemeRequest ( request: android.webkit.WebResourceRequest, response: android.webkit.WebResourceResponse, diff --git a/src/android/window.kt b/src/android/window.kt index 7fbd3ba086..1d7f17844b 100644 --- a/src/android/window.kt +++ b/src/android/window.kt @@ -15,6 +15,7 @@ open class Window (runtime: Runtime, activity: MainActivity) { val activity = WeakReference(activity) val runtime = WeakReference(runtime) val pointer = alloc(bridge.pointer) + var isLoading = false fun evaluateJavaScript (source: String) { this.activity.get()?.evaluateJavaScript(source) @@ -24,6 +25,13 @@ open class Window (runtime: Runtime, activity: MainActivity) { return this.activity.get()?.getRootDirectory() ?: "" } + fun injectPreload (view: android.webkit.WebView) { + view.pauseTimers() + val source = this.getJavaScriptPreloadSource() + this.activity.get()?.evaluateJavaScript(source) + view.resumeTimers() + } + fun load () { val runtime = this.runtime.get() ?: return val isDebugEnabled = this.runtime.get()?.isDebugEnabled() ?: false @@ -31,7 +39,7 @@ open class Window (runtime: Runtime, activity: MainActivity) { val activity = this.activity.get() ?: return val rootDirectory = this.getRootDirectory() - this.bridge.route("ipc://internal.setcwd?value=${rootDirectory}", null, fun (result: Result) { + this.bridge.route("ipc://internal.setcwd?value=${rootDirectory}", null, fun (_: Result) { activity.applicationContext .getSharedPreferences("WebSettings", android.app.Activity.MODE_PRIVATE) .edit() @@ -116,10 +124,27 @@ open class Window (runtime: Runtime, activity: MainActivity) { url: String, bitmap: android.graphics.Bitmap? ) { - val source = this.getJavaScriptPreloadSource() - view.pauseTimers() - this.activity.get()?.evaluateJavaScript(source) - view.resumeTimers() + if (!this.isLoading) { + this.isLoading = true + this.injectPreload(view) + } + } + + open fun onPageFinished ( + view: android.webkit.WebView, + url: String + ) { + this.isLoading = false + } + + open fun onProgressChanged ( + view: android.webkit.WebView, + progress: Int + ) { + if (!this.isLoading && progress > 10) { + this.isLoading = true + this.injectPreload(view) + } } @Throws(java.lang.Exception::class) From 96d3c001dc33a46f7b5abaccfd93be2adcf6b710 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 3 Oct 2023 13:35:37 -0400 Subject: [PATCH 027/256] fix(api/hooks.js): dispatch 'ready' _after_ runtime init --- api/hooks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/hooks.js b/api/hooks.js index e83e35b2c9..2400da6f10 100644 --- a/api/hooks.js +++ b/api/hooks.js @@ -243,14 +243,15 @@ export class Hooks extends EventTarget { // prior to hook initialization if (isRuntimeInitialized) { dispatchLoadEvent(this) - dispatchReadyEvent(this) dispatchInitEvent(this) + dispatchReadyEvent(this) return } addEventListenerOnce(global, RUNTIME_INIT_EVENT_NAME, () => { isRuntimeInitialized = true dispatchInitEvent(this) + dispatchReadyEvent(this) }) if (!isWorkerContext && readyState !== 'complete') { @@ -259,7 +260,6 @@ export class Hooks extends EventTarget { isGlobalLoaded = true dispatchLoadEvent(this) - dispatchReadyEvent(this) } /** From 753edb9b4fe6a577f3516d9af7720ddeb0417c6f Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 3 Oct 2023 14:05:00 -0400 Subject: [PATCH 028/256] chore(api/geolocation): fix docs --- api/index.d.ts | 14 ++++++-------- api/internal/geolocation.js | 6 ++++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/api/index.d.ts b/api/index.d.ts index 2b2841a285..5232166cba 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -6072,21 +6072,19 @@ declare module "socket:internal/geolocation" { /** * @param {function(GeolocationPosition)} onSuccess * @param {onError(Error)} onError - * @param {?({ timeout?: number })} [options] + * @param {object=} options + * @param {number=} options.timeout * @return {Promise} */ - export function getCurrentPosition(onSuccess: (arg0: GeolocationPosition) => any, onError: any, options?: ({ - timeout?: number; - }) | null, ...args: any[]): Promise<any>; + export function getCurrentPosition(onSuccess: (arg0: GeolocationPosition) => any, onError: any, options?: object | undefined, ...args: any[]): Promise<any>; /** * @param {function(GeolocationPosition)} onSuccess * @param {function(Error)} onError - * @param {?({ timeout?: number })} [options] + * @param {object=} options + * @param {number=} options.timeout * @return {Promise} */ - export function watchPosition(onSuccess: (arg0: GeolocationPosition) => any, onError: (arg0: Error) => any, options?: ({ - timeout?: number; - }) | null, ...args: any[]): Promise<any>; + export function watchPosition(onSuccess: (arg0: GeolocationPosition) => any, onError: (arg0: Error) => any, options?: object | undefined, ...args: any[]): Promise<any>; export function clearWatch(id: any, ...args: any[]): any; export namespace platform { let getCurrentPosition: Function; diff --git a/api/internal/geolocation.js b/api/internal/geolocation.js index a757802519..9ff5a4af97 100644 --- a/api/internal/geolocation.js +++ b/api/internal/geolocation.js @@ -127,7 +127,8 @@ export const platform = { /** * @param {function(GeolocationPosition)} onSuccess * @param {onError(Error)} onError - * @param {?({ timeout?: number })} [options] + * @param {object=} options + * @param {number=} options.timeout * @return {Promise} */ export async function getCurrentPosition ( @@ -212,7 +213,8 @@ export async function getCurrentPosition ( /** * @param {function(GeolocationPosition)} onSuccess * @param {function(Error)} onError - * @param {?({ timeout?: number })} [options] + * @param {object=} options + * @param {number=} options.timeout * @return {Promise} */ export function watchPosition ( From 4d5bafc14ba1faf27774e3adfacf75dc810889ab Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Tue, 3 Oct 2023 21:19:43 +0200 Subject: [PATCH 029/256] fix(scr/cli/templates.hh): move watch option to the [webview] section --- api/CONFIG.md | 2 +- src/cli/templates.hh | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/api/CONFIG.md b/api/CONFIG.md index 4aa115f043..27d453d867 100644 --- a/api/CONFIG.md +++ b/api/CONFIG.md @@ -57,6 +57,7 @@ Key | Default Value | Description :--- | :--- | :--- root | "/" | Make root open index.html default_index | "" | Set default 'index.html' path to open for implicit routes +watch | false | Enable watch mode # Section `permissions` @@ -73,7 +74,6 @@ allow_clipboard | true | Allow/Disallow clipboard in application allow_bluetooth | true | Allow/Disallow bluetooth in application allow_data_access | true | Allow/Disallow data access in application allow_airplay | true | Allow/Disallow AirPlay access in application (macOS/iOS) only -watch | false | Enable watch mode # Section `debug` diff --git a/src/cli/templates.hh b/src/cli/templates.hh index 0015fd102d..80a76664a0 100644 --- a/src/cli/templates.hh +++ b/src/cli/templates.hh @@ -1483,6 +1483,11 @@ root = "/" ; default value: "" ; default_index = "" +; Enable watch mode +; default value: false +watch = false + + [permissions] ; Allow/Disallow fullscreen in application ; default value: true @@ -1528,10 +1533,6 @@ root = "/" ; default value: true ; allow_airplay = true -; Enable watch mode -; default value: false -watch = false - [debug] ; Advanced Compiler Settings for debug purposes (ie C++ compiler -g, etc). From 50cce72eebc0dfeddded4abecab85f11d9bd85d9 Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Tue, 3 Oct 2023 22:27:53 +0200 Subject: [PATCH 030/256] docs: update docs --- api/README.md | 130 +++++++++++++++++++++++---------------------- api/crypto.js | 10 ++-- api/errors.js | 14 ++--- api/fs/dir.js | 4 +- api/fs/handle.js | 4 +- api/fs/index.js | 28 ++++++---- api/fs/promises.js | 8 +-- api/index.d.ts | 107 +++++++++++++++++++++++++------------ api/os.js | 17 ++++++ api/process.js | 8 +++ api/test/index.js | 3 ++ api/window.js | 3 ++ 12 files changed, 211 insertions(+), 125 deletions(-) diff --git a/api/README.md b/api/README.md index d99a4493d6..8f6a8d25f9 100644 --- a/api/README.md +++ b/api/README.md @@ -318,6 +318,7 @@ External docs: https://nodejs.org/api/buffer.html ## [webcrypto](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L27) +External docs: https://developer.mozilla.org/en-US/docs/Web/API/Crypto WebCrypto API ## [ready](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L56) @@ -337,24 +338,27 @@ Generate cryptographically strong random values into the `buffer` | :--- | :--- | :--- | | Not specified | TypedArray | | -## [`rand64()`](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L90) +## [`rand64()`](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L94) -This is a `FunctionDeclaration` named `rand64` in `api/crypto.js`, it's exported but undocumented. +Generate a random 64-bit number. +| Return Value | Type | Description | +| :--- | :--- | :--- | +| A random 64-bit number. | BigInt | | -## [RANDOM_BYTES_QUOTA](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L98) +## [RANDOM_BYTES_QUOTA](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L102) Maximum total size of random bytes per page -## [MAX_RANDOM_BYTES](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L103) +## [MAX_RANDOM_BYTES](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L107) Maximum total size for random bytes. -## [MAX_RANDOM_BYTES_PAGES](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L108) +## [MAX_RANDOM_BYTES_PAGES](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L112) Maximum total amount of allocated per page of bytes (max/quota) -## [`randomBytes(size)`](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L116) +## [`randomBytes(size)`](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L120) Generate `size` random bytes. @@ -366,7 +370,7 @@ Generate `size` random bytes. | :--- | :--- | :--- | | Not specified | Buffer | A promise that resolves with an instance of socket.Buffer with random bytes. | -## [`createDigest(algorithm, message)`](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L143) +## [`createDigest(algorithm, message)`](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L147) @@ -731,7 +735,7 @@ External docs: https://nodejs.org/api/events.html ## [`access(path, mode , callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L84) -External docs: https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsopenpath-flags-mode-callback +External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsopenpath-flags-mode-callback Asynchronously check access a file for a given mode calling `callback` upon success or error. @@ -757,6 +761,7 @@ Asynchronously changes the permissions of a file. ## [`close(fd, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L166) +External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsclosefd-callback Asynchronously close a file descriptor calling `callback` upon success or error. | Argument | Type | Default | Optional | Description | @@ -764,14 +769,21 @@ Asynchronously close a file descriptor calling `callback` upon success or error. | fd | number | | false | | | callback | function(Error?)? | | false | | -## [`copyFile()`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L182) +## [`copyFile(src, dest, flags, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L190) -This is a `FunctionDeclaration` named `copyFile` in `api/fs/index.js`, it's exported but undocumented. +External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscopyfilesrc-dest-mode-callback +Asynchronously copies `src` to `dest` calling `callback` upon success or error. +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| src | string | | false | The source file path. | +| dest | string | | false | The destination file path. | +| flags | number | | false | Modifiers for copy operation. | +| callback | function(Error=) | | true | The function to call after completion. | -## [`createReadStream(path, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L210) +## [`createReadStream(path, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L218) -External docs: https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fscreatewritestreampath-options +External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscreatewritestreampath-options | Argument | Type | Default | Optional | Description | @@ -783,9 +795,9 @@ External docs: https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fscreatewri | :--- | :--- | :--- | | Not specified | ReadStream | | -## [`createWriteStream(path, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L250) +## [`createWriteStream(path, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L258) -External docs: https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fscreatewritestreampath-options +External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscreatewritestreampath-options | Argument | Type | Default | Optional | Description | @@ -797,9 +809,9 @@ External docs: https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fscreatewri | :--- | :--- | :--- | | Not specified | WriteStream | | -## [`fstat(fd, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L294) +## [`fstat(fd, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L302) -External docs: https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsfstatfd-options-callback +External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsfstatfd-options-callback Invokes the callback with the <fs.Stats> for the file descriptor. See the POSIX fstat(2) documentation for more detail. @@ -811,8 +823,9 @@ Invokes the callback with the <fs.Stats> for the file descriptor. See | options | object? \| function? | | false | An options object. | | callback | function? | | false | The function to call after completion. | -## [`open(path, flags , mode , options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L399) +## [`open(path, flags , mode , options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L407) +External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsopenpath-flags-mode-callback Asynchronously open a file calling `callback` upon success or error. | Argument | Type | Default | Optional | Description | @@ -823,8 +836,9 @@ Asynchronously open a file calling `callback` upon success or error. | options | object? \| function? | | false | | | callback | function(Error?, number?)? | | false | | -## [`opendir(path, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L452) +## [`opendir(path, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L460) +External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreaddirpath-options-callback Asynchronously open a directory calling `callback` upon success or error. | Argument | Type | Default | Optional | Description | @@ -835,8 +849,9 @@ Asynchronously open a directory calling `callback` upon success or error. | options.withFileTypes | boolean? | false | false | | | callback | function(Error?, Dir?)? | | false | | -## [`read(fd, buffer, offset, length, position, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L478) +## [`read(fd, buffer, offset, length, position, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L486) +External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreadfd-buffer-offset-length-position-callback Asynchronously read from an open file descriptor. | Argument | Type | Default | Optional | Description | @@ -848,8 +863,9 @@ Asynchronously read from an open file descriptor. | position | number \| BigInt \| null | | false | Specifies where to begin reading from in the file. If position is null or -1 , data will be read from the current file position, and the file position will be updated. If position is an integer, the file position will be unchanged. | | callback | function(Error?, number?, Buffer?) | | false | | -## [`readdir(path, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L512) +## [`readdir(path, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L520) +External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreaddirpath-options-callback Asynchronously read all entries in a directory. | Argument | Type | Default | Optional | Description | @@ -860,7 +876,7 @@ Asynchronously read all entries in a directory. | options.withFileTypes ? false | boolean? | | false | | | callback | function(Error?, object) | | false | | -## [`readFile(path, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L563) +## [`readFile(path, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L571) @@ -873,7 +889,7 @@ Asynchronously read all entries in a directory. | options.signal | AbortSignal? | | false | | | callback | function(Error?, Buffer?) | | false | | -## [`stat(path, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L682) +## [`stat(path, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L690) @@ -886,7 +902,7 @@ Asynchronously read all entries in a directory. | options.signal | AbortSignal? | | false | | | callback | function(Error?, Stats?) | | false | | -## [`writeFile(path, data, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L778) +## [`writeFile(path, data, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L786) @@ -928,7 +944,7 @@ Asynchronously read all entries in a directory. ## [`access(path, mode, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L86) -External docs: https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromisesaccesspath-mode +External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesaccesspath-mode Asynchronously check access a file. | Argument | Type | Default | Optional | Description | @@ -999,7 +1015,7 @@ External docs: https://nodejs.org/api/fs.html#fspromisesopendirpath-options ## [`readdir(path, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L260) -External docs: https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromisesreaddirpath-options +External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesreaddirpath-options | Argument | Type | Default | Optional | Description | @@ -1011,7 +1027,7 @@ External docs: https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromisesr ## [`readFile(path, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L293) -External docs: https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromisesreadfilepath-options +External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesreadfilepath-options | Argument | Type | Default | Optional | Description | @@ -1043,7 +1059,7 @@ External docs: https://nodejs.org/api/fs.html#fspromisesstatpath-options ## [`writeFile(path, data, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L446) -External docs: https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromiseswritefilefile-data-options +External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromiseswritefilefile-data-options | Argument | Type | Default | Optional | Description | @@ -1213,12 +1229,11 @@ Returns the operating system name. The operating system's end-of-line marker. `'\r\n'` on Windows and `'\n'` on POSIX. -## [`rusage()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L302) - -This is a `FunctionDeclaration` named `rusage` in `api/os.js`, it's exported but undocumented. +## [`rusage()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L306) +Get resource usage. -## [`uptime()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L312) +## [`uptime()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L316) Returns the system uptime in seconds. @@ -1226,20 +1241,13 @@ Returns the system uptime in seconds. | :--- | :--- | :--- | | Not specified | number | The system uptime in seconds. | -## [`uname()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L318) - -This is a `FunctionDeclaration` named `uname` in `api/os.js`, it's exported but undocumented. +## [`uname()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L327) +Returns the operating system name. -## [`hrtime()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L328) - -This is a `FunctionDeclaration` named `hrtime` in `api/os.js`, it's exported but undocumented. - - -## [`availableMemory()`](https://github.com/socketsupply/socket/blob/master/api/os.js#L337) - -This is a `FunctionDeclaration` named `availableMemory` in `api/os.js`, it's exported but undocumented. - +| Return Value | Type | Description | +| :--- | :--- | :--- | +| Not specified | string | The operating system name. | # [Path](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L9) @@ -1475,12 +1483,15 @@ Converts this `Path` instance to a string. import process from 'socket:process' ``` -## [`nextTick()`](https://github.com/socketsupply/socket/blob/master/api/process.js#L38) +## [`nextTick(callback)`](https://github.com/socketsupply/socket/blob/master/api/process.js#L42) -This is a `FunctionDeclaration` named `nextTick` in `api/process.js`, it's exported but undocumented. +Adds callback to the 'nextTick' queue. +| Argument | Type | Default | Optional | Description | +| :--- | :--- | :---: | :---: | :--- | +| callback | Function | | false | | -## [`homedir()`](https://github.com/socketsupply/socket/blob/master/api/process.js#L67) +## [`homedir()`](https://github.com/socketsupply/socket/blob/master/api/process.js#L71) @@ -1488,7 +1499,7 @@ This is a `FunctionDeclaration` named `nextTick` in `api/process.js`, it's expor | :--- | :--- | :--- | | Not specified | string | The home directory of the current user. | -## [`hrtime(time)`](https://github.com/socketsupply/socket/blob/master/api/process.js#L76) +## [`hrtime(time)`](https://github.com/socketsupply/socket/blob/master/api/process.js#L80) Computed high resolution time as a `BigInt`. @@ -1500,7 +1511,7 @@ Computed high resolution time as a `BigInt`. | :--- | :--- | :--- | | Not specified | bigint | | -## [`exit(code)`](https://github.com/socketsupply/socket/blob/master/api/process.js#L100) +## [`exit(code)`](https://github.com/socketsupply/socket/blob/master/api/process.js#L104) @@ -1508,10 +1519,13 @@ Computed high resolution time as a `BigInt`. | :--- | :--- | :---: | :---: | :--- | | code | number | 0 | true | The exit code. Default: 0. | -## [`memoryUsage()`](https://github.com/socketsupply/socket/blob/master/api/process.js#L108) +## [`memoryUsage()`](https://github.com/socketsupply/socket/blob/master/api/process.js#L116) -This is a `FunctionDeclaration` named `memoryUsage` in `api/process.js`, it's exported but undocumented. +Returns an object describing the memory usage of the Node.js process measured in bytes. +| Return Value | Type | Description | +| :--- | :--- | :--- | +| Not specified | Object | | # [Test](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L17) @@ -2035,12 +2049,7 @@ Retrieves the computed styles for a given element. | :--- | :--- | :---: | :---: | :--- | | ) | (result: { total: number, success: number, fail: number | > void} callback | false | | -## [GLOBAL_TEST_RUNNER](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1048) - -This is a `VariableDeclaration` named `GLOBAL_TEST_RUNNER` in `api/test/index.js`, it's exported but undocumented. - - -## [`only(name, fn)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1057) +## [`only(name, fn)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1060) @@ -2049,7 +2058,7 @@ This is a `VariableDeclaration` named `GLOBAL_TEST_RUNNER` in `api/test/index.js | name | string | | false | | | fn | TestFn | | false | | -## [`skip(_name, _fn)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1067) +## [`skip(_name, _fn)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1070) @@ -2058,7 +2067,7 @@ This is a `VariableDeclaration` named `GLOBAL_TEST_RUNNER` in `api/test/index.js | _name | string | | false | | | _fn | TestFn | | false | | -## [`setStrict(strict)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1073) +## [`setStrict(strict)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1076) @@ -2324,8 +2333,3 @@ Removes a listener from the window. An alias for `removeListener`. | event | string | | false | the event to remove the listener from | | cb | function(*): void | | false | the callback to remove | -## [constants](https://github.com/socketsupply/socket/blob/master/api/window.js#L409) - -This is a `VariableDeclaration` named `constants` in `api/window.js`, it's exported but undocumented. - - diff --git a/api/crypto.js b/api/crypto.js index 379aa249aa..35b9239637 100644 --- a/api/crypto.js +++ b/api/crypto.js @@ -22,7 +22,7 @@ import * as exports from './crypto.js' /** * WebCrypto API - * @see {https://developer.mozilla.org/en-US/docs/Web/API/Crypto} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Crypto} */ export let webcrypto = globalThis.crypto?.webcrypto ?? globalThis.crypto @@ -57,8 +57,8 @@ export const ready = Promise.all(pending) /** * libsodium API - * @see {https://doc.libsodium.org/} - * @see {https://github.com/jedisct1/libsodium.js} + * @see {@link https://doc.libsodium.org/} + * @see {@link https://github.com/jedisct1/libsodium.js} */ export { sodium } @@ -87,6 +87,10 @@ export function getRandomValues (buffer, ...args) { // so this is re-used instead of creating new one each rand64() call const tmp = new Uint32Array(2) +/** + * Generate a random 64-bit number. + * @returns {BigInt} - A random 64-bit number. + */ export function rand64 () { getRandomValues(tmp) return (BigInt(tmp[0]) << 32n) | BigInt(tmp[1]) diff --git a/api/errors.js b/api/errors.js index e672682dbb..99f587de12 100644 --- a/api/errors.js +++ b/api/errors.js @@ -20,7 +20,7 @@ export const TIMEOUT_ERR = 23 export class AbortError extends Error { /** * The code given to an `ABORT_ERR` `DOMException` - * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} */ static get code () { return ABORT_ERR } @@ -268,7 +268,7 @@ export class InternalError extends Error { export class InvalidAccessError extends Error { /** * The code given to an `INVALID_ACCESS_ERR` `DOMException` - * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} */ static get code () { return INVALID_ACCESS_ERR } @@ -301,7 +301,7 @@ export class InvalidAccessError extends Error { export class NetworkError extends Error { /** * The code given to an `NETWORK_ERR` `DOMException` - * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} */ static get code () { return NETWORK_ERR } @@ -336,7 +336,7 @@ export class NetworkError extends Error { export class NotAllowedError extends Error { /** * The code given to an `NOT_ALLOWED_ERR` `DOMException` - * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} */ static get code () { return NOT_ALLOWED_ERR } @@ -369,7 +369,7 @@ export class NotAllowedError extends Error { export class NotFoundError extends Error { /** * The code given to an `NOT_FOUND_ERR` `DOMException` - * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} */ static get code () { return NOT_FOUND_ERR } @@ -402,7 +402,7 @@ export class NotFoundError extends Error { export class NotSupportedError extends Error { /** * The code given to an `NOT_SUPPORTED_ERR` `DOMException` - * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} */ static get code () { return NOT_SUPPORTED_ERR } @@ -494,7 +494,7 @@ export class OperationError extends Error { export class TimeoutError extends Error { /** * The code given to an `TIMEOUT_ERR` `DOMException` - * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} */ static get code () { return TIMEOUT_ERR } diff --git a/api/fs/dir.js b/api/fs/dir.js index 002373d586..3f816c31db 100644 --- a/api/fs/dir.js +++ b/api/fs/dir.js @@ -37,7 +37,7 @@ export function sortDirectoryEntries (a, b) { * A containerr for a directory and its entries. This class supports scanning * a directory entry by entry with a `read()` method. The `Symbol.asyncIterator` * interface is exposed along with an AsyncGenerator `entries()` method. - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#class-fsdir} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#class-fsdir} */ export class Dir { static from (fdOrHandle, options) { @@ -179,7 +179,7 @@ export class Dir { /** * A container for a directory entry. - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#class-fsdirent} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#class-fsdirent} */ export class Dirent { static get UNKNOWN () { return UV_DIRENT_UNKNOWN } diff --git a/api/fs/handle.js b/api/fs/handle.js index 81b9ac3c42..3e3adca70b 100644 --- a/api/fs/handle.js +++ b/api/fs/handle.js @@ -43,7 +43,7 @@ export const kClosed = Symbol.for('fs.FileHandle.closed') /** * A container for a descriptor tracked in `fds` and opened in the native layer. * This class implements the Node.js `FileHandle` interface - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#class-filehandle} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#class-filehandle} */ export class FileHandle extends EventEmitter { static get DEFAULT_ACCESS_MODE () { return F_OK } @@ -109,7 +109,7 @@ export class FileHandle extends EventEmitter { /** * Asynchronously open a file. - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromisesopenpath-flags-mode} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesopenpath-flags-mode} * @param {string | Buffer | URL} path * @param {string=} [flags = 'r'] * @param {string=} [mode = 0o666] diff --git a/api/fs/index.js b/api/fs/index.js index e85f265167..7337b2c0ba 100644 --- a/api/fs/index.js +++ b/api/fs/index.js @@ -76,7 +76,7 @@ async function visit (path, options, callback) { /** * Asynchronously check access a file for a given mode calling `callback` * upon success or error. - * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsopenpath-flags-mode-callback} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsopenpath-flags-mode-callback} * @param {string | Buffer | URL} path * @param {string?|function(Error?)?} [mode = F_OK(0)] * @param {function(Error?)?} [callback] @@ -159,7 +159,7 @@ export function chown (path, uid, gid, callback) { /** * Asynchronously close a file descriptor calling `callback` upon success or error. - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsclosefd-callback} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsclosefd-callback} * @param {number} fd * @param {function(Error?)?} [callback] */ @@ -179,6 +179,14 @@ export function close (fd, callback) { } } +/** + * Asynchronously copies `src` to `dest` calling `callback` upon success or error. + * @param {string} src - The source file path. + * @param {string} dest - The destination file path. + * @param {number} flags - Modifiers for copy operation. + * @param {function(Error=)=} [callback] - The function to call after completion. + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscopyfilesrc-dest-mode-callback} + */ export function copyFile (src, dest, flags, callback) { if (typeof src !== 'string') { throw new TypeError('The argument \'src\' must be a string') @@ -202,7 +210,7 @@ export function copyFile (src, dest, flags, callback) { } /** - * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fscreatewritestreampath-options} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscreatewritestreampath-options} * @param {string | Buffer | URL} path * @param {object?} [options] * @returns {ReadStream} @@ -242,7 +250,7 @@ export function createReadStream (path, options) { } /** - * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fscreatewritestreampath-options} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscreatewritestreampath-options} * @param {string | Buffer | URL} path * @param {object?} [options] * @returns {WriteStream} @@ -285,7 +293,7 @@ export function createWriteStream (path, options) { * Invokes the callback with the <fs.Stats> for the file descriptor. See * the POSIX fstat(2) documentation for more detail. * - * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsfstatfd-options-callback} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsfstatfd-options-callback} * * @param {number} fd - A file descriptor. * @param {object?|function?} [options] - An options object. @@ -389,7 +397,7 @@ export function mkdir (path, options, callback) { /** * Asynchronously open a file calling `callback` upon success or error. - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsopenpath-flags-mode-callback} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsopenpath-flags-mode-callback} * @param {string | Buffer | URL} path * @param {string?} [flags = 'r'] * @param {string?} [mode = 0o666] @@ -442,7 +450,7 @@ export function open (path, flags = 'r', mode = 0o666, options = null, callback) /** * Asynchronously open a directory calling `callback` upon success or error. - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsreaddirpath-options-callback} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreaddirpath-options-callback} * @param {string | Buffer | URL} path * @param {object?|function(Error?, Dir?)} [options] * @param {string?} [options.encoding = 'utf8'] @@ -467,7 +475,7 @@ export function opendir (path, options = {}, callback) { /** * Asynchronously read from an open file descriptor. - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsreadfd-buffer-offset-length-position-callback} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreadfd-buffer-offset-length-position-callback} * @param {number} fd * @param {object | Buffer | TypedArray} buffer - The buffer that the data will be written to. * @param {number} offset - The position in buffer to write the data to. @@ -502,7 +510,7 @@ export function read (fd, buffer, offset, length, position, options, callback) { /** * Asynchronously read all entries in a directory. - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsreaddirpath-options-callback} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreaddirpath-options-callback} * @param {string | Buffer | URL } path * @param {object?|function(Error?, object[])} [options] * @param {string?} [options.encoding ? 'utf8'] @@ -765,7 +773,7 @@ export function unlink (path, callback) { } /** - * @see {@url https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fswritefilefile-data-options-callback} + * @see {@url https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fswritefilefile-data-options-callback} * @param {string | Buffer | URL | number } path - filename or file descriptor * @param {string | Buffer | TypedArray | DataView | object } data * @param {object?} options diff --git a/api/fs/promises.js b/api/fs/promises.js index 9184eaddd7..a1073dcd5f 100644 --- a/api/fs/promises.js +++ b/api/fs/promises.js @@ -78,7 +78,7 @@ async function visit (path, options, callback) { /** * Asynchronously check access a file. - * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromisesaccesspath-mode} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesaccesspath-mode} * @param {string | Buffer | URL} path * @param {string?} [mode] * @param {object?} [options] @@ -251,7 +251,7 @@ export async function opendir (path, options) { } /** - * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromisesreaddirpath-options} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesreaddirpath-options} * @param {string | Buffer | URL} path * @param {object?} options * @param {string?} [options.encoding = 'utf8'] @@ -282,7 +282,7 @@ export async function readdir (path, options) { } /** - * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromisesreadfilepath-options} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesreadfilepath-options} * @param {string} path * @param {object?} [options] * @param {(string|null)?} [options.encoding = null] @@ -432,7 +432,7 @@ export async function unlink (path) { } /** - * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromiseswritefilefile-data-options} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromiseswritefilefile-data-options} * @param {string | Buffer | URL | FileHandle} path - filename or FileHandle * @param {string|Buffer|Array|DataView|TypedArray} data * @param {object?} [options] diff --git a/api/index.d.ts b/api/index.d.ts index 5232166cba..b829db5cda 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -17,7 +17,7 @@ declare module "socket:errors" { export class AbortError extends Error { /** * The code given to an `ABORT_ERR` `DOMException` - * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} */ static get code(): number; /** @@ -154,7 +154,7 @@ declare module "socket:errors" { export class InvalidAccessError extends Error { /** * The code given to an `INVALID_ACCESS_ERR` `DOMException` - * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} */ static get code(): number; /** @@ -173,7 +173,7 @@ declare module "socket:errors" { export class NetworkError extends Error { /** * The code given to an `NETWORK_ERR` `DOMException` - * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} */ static get code(): number; /** @@ -192,7 +192,7 @@ declare module "socket:errors" { export class NotAllowedError extends Error { /** * The code given to an `NOT_ALLOWED_ERR` `DOMException` - * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} */ static get code(): number; /** @@ -211,7 +211,7 @@ declare module "socket:errors" { export class NotFoundError extends Error { /** * The code given to an `NOT_FOUND_ERR` `DOMException` - * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} */ static get code(): number; /** @@ -230,7 +230,7 @@ declare module "socket:errors" { export class NotSupportedError extends Error { /** * The code given to an `NOT_SUPPORTED_ERR` `DOMException` - * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} */ static get code(): number; /** @@ -278,7 +278,7 @@ declare module "socket:errors" { export class TimeoutError extends Error { /** * The code given to an `TIMEOUT_ERR` `DOMException` - * @see {https://developer.mozilla.org/en-US/docs/Web/API/DOMException} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/DOMException} */ static get code(): number; /** @@ -704,14 +704,29 @@ declare module "socket:os" { * @returns {string} - The operating system's default directory for temporary files. */ export function tmpdir(): string; + /** + * Get resource usage. + */ export function rusage(): any; /** * Returns the system uptime in seconds. * @returns {number} - The system uptime in seconds. */ export function uptime(): number; + /** + * Returns the operating system name. + * @returns {string} - The operating system name. + */ export function uname(): string; + /** + * It's implemented in process.hrtime.bigint() + * @ignore + */ export function hrtime(): any; + /** + * Node.js doesn't have this method. + * @ignore + */ export function availableMemory(): any; /** * @type {string} @@ -723,7 +738,11 @@ declare module "socket:os" { } declare module "socket:process" { - export function nextTick(callback: any): void; + /** + * Adds callback to the 'nextTick' queue. + * @param {Function} callback + */ + export function nextTick(callback: Function): void; /** * @returns {string} The home directory of the current user. */ @@ -741,9 +760,11 @@ declare module "socket:process" { * @param {number=} [code=0] - The exit code. Default: 0. */ export function exit(code?: number | undefined): Promise<void>; - export function memoryUsage(): { - rss: any; - }; + /** + * Returns an object describing the memory usage of the Node.js process measured in bytes. + * @returns {Object} + */ + export function memoryUsage(): any; export namespace memoryUsage { function rss(): any; } @@ -1951,7 +1972,7 @@ declare module "socket:fs/handle" { /** * A container for a descriptor tracked in `fds` and opened in the native layer. * This class implements the Node.js `FileHandle` interface - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#class-filehandle} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#class-filehandle} */ export class FileHandle extends EventEmitter { static get DEFAULT_ACCESS_MODE(): any; @@ -1973,7 +1994,7 @@ declare module "socket:fs/handle" { static access(path: string, mode?: number, options?: object | undefined): boolean; /** * Asynchronously open a file. - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromisesopenpath-flags-mode} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesopenpath-flags-mode} * @param {string | Buffer | URL} path * @param {string=} [flags = 'r'] * @param {string=} [mode = 0o666] @@ -2227,7 +2248,7 @@ declare module "socket:fs/dir" { * A containerr for a directory and its entries. This class supports scanning * a directory entry by entry with a `read()` method. The `Symbol.asyncIterator` * interface is exposed along with an AsyncGenerator `entries()` method. - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#class-fsdir} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#class-fsdir} */ export class Dir { static from(fdOrHandle: any, options: any): exports.Dir; @@ -2266,7 +2287,7 @@ declare module "socket:fs/dir" { } /** * A container for a directory entry. - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#class-fsdirent} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#class-fsdirent} */ export class Dirent { static get UNKNOWN(): any; @@ -2332,7 +2353,7 @@ declare module "socket:fs/dir" { declare module "socket:fs/promises" { /** * Asynchronously check access a file. - * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromisesaccesspath-mode} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesaccesspath-mode} * @param {string | Buffer | URL} path * @param {string?} [mode] * @param {object?} [options] @@ -2390,7 +2411,7 @@ declare module "socket:fs/promises" { */ export function opendir(path: string | Buffer | URL, options?: object | null): Promise<Dir>; /** - * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromisesreaddirpath-options} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesreaddirpath-options} * @param {string | Buffer | URL} path * @param {object?} options * @param {string?} [options.encoding = 'utf8'] @@ -2398,7 +2419,7 @@ declare module "socket:fs/promises" { */ export function readdir(path: string | Buffer | URL, options: object | null): Promise<any[]>; /** - * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromisesreadfilepath-options} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesreadfilepath-options} * @param {string} path * @param {object?} [options] * @param {(string|null)?} [options.encoding = null] @@ -2440,7 +2461,7 @@ declare module "socket:fs/promises" { */ export function unlink(path: any): Promise<void>; /** - * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fspromiseswritefilefile-data-options} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromiseswritefilefile-data-options} * @param {string | Buffer | URL | FileHandle} path - filename or FileHandle * @param {string|Buffer|Array|DataView|TypedArray} data * @param {object?} [options] @@ -2472,7 +2493,7 @@ declare module "socket:fs/index" { /** * Asynchronously check access a file for a given mode calling `callback` * upon success or error. - * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsopenpath-flags-mode-callback} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsopenpath-flags-mode-callback} * @param {string | Buffer | URL} path * @param {string?|function(Error?)?} [mode = F_OK(0)] * @param {function(Error?)?} [callback] @@ -2500,21 +2521,29 @@ declare module "socket:fs/index" { export function chown(path: any, uid: any, gid: any, callback: any): void; /** * Asynchronously close a file descriptor calling `callback` upon success or error. - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsclosefd-callback} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsclosefd-callback} * @param {number} fd * @param {function(Error?)?} [callback] */ export function close(fd: number, callback?: ((arg0: Error | null) => any) | null): void; - export function copyFile(src: any, dest: any, flags: any, callback: any): void; /** - * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fscreatewritestreampath-options} + * Asynchronously copies `src` to `dest` calling `callback` upon success or error. + * @param {string} src - The source file path. + * @param {string} dest - The destination file path. + * @param {number} flags - Modifiers for copy operation. + * @param {function(Error=)=} [callback] - The function to call after completion. + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscopyfilesrc-dest-mode-callback} + */ + export function copyFile(src: string, dest: string, flags: number, callback?: ((arg0: Error | undefined) => any) | undefined): void; + /** + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscreatewritestreampath-options} * @param {string | Buffer | URL} path * @param {object?} [options] * @returns {ReadStream} */ export function createReadStream(path: string | Buffer | URL, options?: object | null): ReadStream; /** - * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fscreatewritestreampath-options} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscreatewritestreampath-options} * @param {string | Buffer | URL} path * @param {object?} [options] * @returns {WriteStream} @@ -2524,7 +2553,7 @@ declare module "socket:fs/index" { * Invokes the callback with the <fs.Stats> for the file descriptor. See * the POSIX fstat(2) documentation for more detail. * - * @see {@link https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsfstatfd-options-callback} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsfstatfd-options-callback} * * @param {number} fd - A file descriptor. * @param {object?|function?} [options] - An options object. @@ -2545,7 +2574,7 @@ declare module "socket:fs/index" { export function mkdir(path: any, options: any, callback: any): void; /** * Asynchronously open a file calling `callback` upon success or error. - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsopenpath-flags-mode-callback} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsopenpath-flags-mode-callback} * @param {string | Buffer | URL} path * @param {string?} [flags = 'r'] * @param {string?} [mode = 0o666] @@ -2555,7 +2584,7 @@ declare module "socket:fs/index" { export function open(path: string | Buffer | URL, flags?: string | null, mode?: string | null, options?: any, callback?: ((arg0: Error | null, arg1: number | null) => any) | null): void; /** * Asynchronously open a directory calling `callback` upon success or error. - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsreaddirpath-options-callback} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreaddirpath-options-callback} * @param {string | Buffer | URL} path * @param {object?|function(Error?, Dir?)} [options] * @param {string?} [options.encoding = 'utf8'] @@ -2565,7 +2594,7 @@ declare module "socket:fs/index" { export function opendir(path: string | Buffer | URL, options: {}, callback: ((arg0: Error | null, arg1: Dir | null) => any) | null): void; /** * Asynchronously read from an open file descriptor. - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsreadfd-buffer-offset-length-position-callback} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreadfd-buffer-offset-length-position-callback} * @param {number} fd * @param {object | Buffer | TypedArray} buffer - The buffer that the data will be written to. * @param {number} offset - The position in buffer to write the data to. @@ -2576,7 +2605,7 @@ declare module "socket:fs/index" { export function read(fd: number, buffer: object | Buffer | TypedArray, offset: number, length: number, position: number | BigInt | null, options: any, callback: (arg0: Error | null, arg1: number | null, arg2: Buffer | null) => any): void; /** * Asynchronously read all entries in a directory. - * @see {https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsreaddirpath-options-callback} + * @see {@link https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsreaddirpath-options-callback} * @param {string | Buffer | URL } path * @param {object?|function(Error?, object[])} [options] * @param {string?} [options.encoding ? 'utf8'] @@ -2628,7 +2657,7 @@ declare module "socket:fs/index" { */ export function unlink(path: any, callback: any): void; /** - * @see {@url https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fswritefilefile-data-options-callback} + * @see {@url https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fswritefilefile-data-options-callback} * @param {string | Buffer | URL | number } path - filename or file descriptor * @param {string | Buffer | TypedArray | DataView | object } data * @param {object?} options @@ -2676,7 +2705,11 @@ declare module "socket:crypto" { * @return {TypedArray} */ export function getRandomValues(buffer: TypedArray, ...args: any[]): TypedArray; - export function rand64(): bigint; + /** + * Generate a random 64-bit number. + * @returns {BigInt} - A random 64-bit number. + */ + export function rand64(): BigInt; /** * Generate `size` random bytes. * @param {number} size - The number of bytes to generate. The size must not be larger than 2**31 - 1. @@ -2694,7 +2727,7 @@ declare module "socket:crypto" { */ /** * WebCrypto API - * @see {https://developer.mozilla.org/en-US/docs/Web/API/Crypto} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Crypto} */ export let webcrypto: any; /** @@ -3307,6 +3340,9 @@ declare module "socket:window" { #private; } export default ApplicationWindow; + /** + * @ignore + */ export const constants: typeof statuses; import ipc from "socket:ipc"; import * as statuses from "socket:window/constants"; @@ -3601,7 +3637,7 @@ declare module "socket:net" { export class Server extends EventEmitter { constructor(options: any, handler: any); _connections: number; - id: bigint; + id: BigInt; onconnection(data: any): void; listen(port: any, address: any, cb: any): this; _address: { @@ -3635,7 +3671,7 @@ declare module "socket:net" { pause(): this; resume(): this; connect(...args: any[]): this; - id: bigint; + id: BigInt; remotePort: any; remoteAddress: any; unref(): this; @@ -4893,6 +4929,9 @@ declare module "socket:test/index" { fail: number; }) => void): void; } + /** + * @ignore + */ export const GLOBAL_TEST_RUNNER: TestRunner; export default test; export type testWithProperties = { diff --git a/api/os.js b/api/os.js index 33a2492ad9..f8bdd11f22 100644 --- a/api/os.js +++ b/api/os.js @@ -299,6 +299,10 @@ export const EOL = (() => { return '\n' })() +// FIXME: should be process.resourceUsage() +/** + * Get resource usage. + */ export function rusage () { const { err, data } = ipc.sendSync('os.rusage') if (err) throw err @@ -315,6 +319,11 @@ export function uptime () { return data } +// FIXME: should be os.machine() + os.release() + os.type() + os.version() +/** + * Returns the operating system name. + * @returns {string} - The operating system name. + */ export function uname () { if (!cache.uname) { const { err, data } = ipc.sendSync('os.uname') @@ -325,6 +334,10 @@ export function uname () { return cache.uname } +/** + * It's implemented in process.hrtime.bigint() + * @ignore + */ export function hrtime () { const result = ipc.sendSync('os.hrtime', {}, { desiredResponseType: 'arraybuffer' @@ -334,6 +347,10 @@ export function hrtime () { return result.data.readBigUInt64BE(0) } +/** + * Node.js doesn't have this method. + * @ignore + */ export function availableMemory () { const result = ipc.sendSync('os.availableMemory', {}, { desiredResponseType: 'arraybuffer' diff --git a/api/process.js b/api/process.js index 2ed9a059de..58c6a78291 100644 --- a/api/process.js +++ b/api/process.js @@ -35,6 +35,10 @@ if (!isNode) { export default process +/** + * Adds callback to the 'nextTick' queue. + * @param {Function} callback + */ export function nextTick (callback) { if (typeof process.nextTick === 'function' && process.nextTick !== nextTick) { process.nextTick(callback) @@ -105,6 +109,10 @@ export async function exit (code) { } } +/** + * Returns an object describing the memory usage of the Node.js process measured in bytes. + * @returns {Object} + */ export function memoryUsage () { const rss = memoryUsage.rss() return { diff --git a/api/test/index.js b/api/test/index.js index 5f0b011ee4..db6b35a8c8 100644 --- a/api/test/index.js +++ b/api/test/index.js @@ -1045,6 +1045,9 @@ function printLine (line) { console.log(line) } +/** + * @ignore + */ export const GLOBAL_TEST_RUNNER = new TestRunner() initContext(GLOBAL_TEST_RUNNER) diff --git a/api/window.js b/api/window.js index c2417f302e..f0c3f08a0d 100644 --- a/api/window.js +++ b/api/window.js @@ -406,4 +406,7 @@ export class ApplicationWindow { } export default ApplicationWindow +/** + * @ignore + */ export const constants = statuses From 161f15a4ddce206c637e0e2aff0dc6182fae10e6 Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Tue, 3 Oct 2023 18:35:53 +0200 Subject: [PATCH 031/256] docs(CONTRIBUTING.md): add build instructions --- CONTRIBUTING.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c31de8d1fe..19161b3659 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,6 +20,40 @@ cd socket .\bin\install.ps1 ``` +## Building `ssc` and other useful commands + +`ssc` (short for Socket Supply Compiler) is a command-line tool that is used to build Socket Runtime applications. +It is a wrapper around the native build tools for each platform and provides a unified interface for building Socket Runtime applications. You don't have to use Xcode, MSAndroid Studio, or Visual Studio to build Socket Runtime applications, but you can if you want to. + +### `./bin/install.sh` (macOS and Linux) or `.\bin\install.ps1` (Windows) + +This command installs the dependencies for Socket Runtime and builds `ssc` for your platform. +- `ssc` on macOS can cross-compile for iOS, iOS Simulator, Android, and Android Emulator. +- `ssc` on Linux can cross-compile for Android and Android Emulator. +- `ssc` on Windows can cross-compile for Android and Android Emulator. + +Additional flags on macOS and Linux: +- `VERBOSE=1` - prints useful information about the build process. Please use this flag if you are reporting a bug. +- `DEBUG=1` - builds `ssc` in debug mode. This is useful if you are developing `ssc` and want to debug it. +- `NO_ANDROID=1` - skips the `ssc`'s Android support. This is useful if you are not developing for Android and don't want to install the Android SDK. +- `NO_IOS=1` - skips the `ssc`'s iOS support. This is useful if you are not developing for iOS and don't want to install the iOS SDK. It's only useful on macOS. +- `CPU_CORES={number}` - sets the number of CPU cores to use for the build. This is useful if you want to speed up the build process. The default value is the number of CPU cores on your machine. +<!-- +TODO(@chicoxyzzy): +- `SOCKET_HOME={path}` - sets the path to the Socket Runtime build directory. This is useful if you want to build Socket Runtime in a different directory than the default one. + +what else? +--> + +Additional flags on Windows: +- `-debug` - builds `ssc` in debug mode. This is useful if you are developing `ssc` and want to debug it. +- `-verbose` - prints useful information about the build process. Please use this flag if you are reporting a bug. +- `-yesdeps` - automatically installs the dependencies for Socket Runtime. + +### `./bin/clean.sh` (macOS and Linux) + +Sometimes you need to clean the build artifacts and start from scratch. This command removes the build artifacts for `ssc` and the Socket Runtime libraries. + ## Project directory structure The project is structured as follows: From 9aeee52eb3e941840eb7f3269584fd17543cf775 Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Tue, 3 Oct 2023 18:49:28 +0200 Subject: [PATCH 032/256] docs(CONTRIBUTING.md): automatic ordering --- CONTRIBUTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 19161b3659..d8b959e942 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,14 +63,14 @@ It consists of the built-in modules that are available in the runtime and the `s These modules have native bindings to the underlying C++/Objective-C/Kotlin code and libuv to expose the platform capabilities to the JavaScript code. -2. [`bin`](https://github.com/socketsupply/socket/tree/master/bin): This directory contains useful scripts for building the project on different platforms, managing versions, generating documentation, publishing npm packages, etc. +1. [`bin`](https://github.com/socketsupply/socket/tree/master/bin): This directory contains useful scripts for building the project on different platforms, managing versions, generating documentation, publishing npm packages, etc. -3. [`npm`](https://github.com/socketsupply/socket/tree/master/npm): This directory consists of the JavaScrip wrappers for the native code, build scripts and package directories. +1. [`npm`](https://github.com/socketsupply/socket/tree/master/npm): This directory consists of the JavaScrip wrappers for the native code, build scripts and package directories. This directory consists of the JavaScrip wrappers for the native code, build scripts and package directories. You can also find the official Socket Runtime Node.js backend in the [`npm/packages/@socketsupply/socket-node`](https://github.com/socketsupply/socket/tree/master/npm/packages/%40socketsupply/socket-node) directory. -4. [`src`](https://github.com/socketsupply/socket/tree/master/src): This directory contains the native code for the Socket Runtime: +1. [`src`](https://github.com/socketsupply/socket/tree/master/src): This directory contains the native code for the Socket Runtime: - [`android`](https://github.com/socketsupply/socket/tree/master/src/android): contains the source code for the Socket Runtime library for Android - [`app`](https://github.com/socketsupply/socket/tree/master/src/app): contains the source code related to the Socket Runtime application instance - [`cli`](https://github.com/socketsupply/socket/tree/master/src/cli): contains the source code for the Socket Runtime CLI @@ -82,7 +82,7 @@ You can also find the official Socket Runtime Node.js backend in the - [`process`](https://github.com/socketsupply/socket/tree/master/src/process): contains the source code for the process management - [`window`](https://github.com/socketsupply/socket/tree/master/src/window): contains the source code for the window management on desktop platforms -5. [`test`](https://github.com/socketsupply/socket/tree/master/src/test): This directory contains the actual Socket Runtime application that is used for testing the native code and the JavaScript API. +1. [`test`](https://github.com/socketsupply/socket/tree/master/src/test): This directory contains the actual Socket Runtime application that is used for testing the native code and the JavaScript API. ## Other repositories From 6468aace774dc282766c8a9864c8e46f61147216 Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Tue, 3 Oct 2023 19:03:37 +0200 Subject: [PATCH 033/256] docs(CONTRIBUTING.md): build apps and run tests --- CONTRIBUTING.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d8b959e942..ced9cf3056 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,6 +84,33 @@ You can also find the official Socket Runtime Node.js backend in the 1. [`test`](https://github.com/socketsupply/socket/tree/master/src/test): This directory contains the actual Socket Runtime application that is used for testing the native code and the JavaScript API. +## Building Socket Runtime applications + +Once you have built `ssc`, you can use it to build Socket Runtime applications. +`ssc -h` command prints the help message for `ssc` and lists all the available commands. + +Check out the [Guides](https://socketsupply.co/guides/) for more information on how to build Socket Runtime applications. + +## Adding tests + +We run Socket Runtime E2E tests from the actual Socket Runtime application located in the [`test`](https://github.com/socketsupply/socket/tree/master/src/test) directory. This allows us to test the Socket Runtime API from the perspective of the Socket Runtime application developer. The tests are written in JavaScript and use the built-in `socket:test` module to test the Socket Runtime API. `socket:test` is a port of the [`tapzero`](https://github.com/socketsupply/tapzero) testing framework. + +To run the tests, use the following command: + +```bash +npm run test +``` +You can also run specific tests against specific module by using the `--test` flag: + +```bash +ssc run --test=application.js ./test +``` +Be sure to build `ssc` before running the tests if you have made any changes to the Socket Runtime source code. +You can also rebuild and run `ssc` with a single command: + +```bash +ssc build -r --test=application.js ./test +``` ## Other repositories - [Socket-Examples](https://github.com/socketsupply/socket-examples): This repository contains example projects powered by Socket which helps you build cross-platform apps for desktop and mobile. From 9b196bf9eeb907bf7b3238c1e3b0a5632baac361 Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Tue, 3 Oct 2023 19:39:06 +0200 Subject: [PATCH 034/256] docs(CONTRIBUTING.md): publish-npm-modules.sh --- CONTRIBUTING.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ced9cf3056..e77e4a559e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,6 +50,20 @@ Additional flags on Windows: - `-verbose` - prints useful information about the build process. Please use this flag if you are reporting a bug. - `-yesdeps` - automatically installs the dependencies for Socket Runtime. +If you make any changes to the Socket Runtime JavaScript API source code, you should run `npm run gen` after running `./bin/install.sh` or `.\bin\install.ps1` to update the TypeScript type definitions for Socket Runtime and the API documentation. + +### `./bin/publish-npm-modules.sh` (macOS and Linux) + +This command runs `./bin/install.sh`, builds `ssc` npm packages and does [`npm link`](https://docs.npmjs.com/cli/v10/commands/npm-link) for the `@socketsupply/socket` package and platform-specific packages (if `--link` flag is provided, see description below). +This allows you to use the latest version of Socket Runtime in your Socket Runtime applications without publishing the npm packages to npmjs.com registry. Just run `npm link @socketsupply/socket` in your Socket Runtime application directory and you are good to go and use your local version of Socket Runtime. + +Useful flags: +- `--only-platforms` - builds only the platform-specific npm packages. +- `--only-top-level` - builds only the top-level npm package. +- `--link` - runs `npm link` for the `@socketsupply/socket` package and platform-specific packages. + +You don't have to run `npm run gen` after running `./bin/publish-npm-modules.sh` because it runs `npm run gen` automatically. + ### `./bin/clean.sh` (macOS and Linux) Sometimes you need to clean the build artifacts and start from scratch. This command removes the build artifacts for `ssc` and the Socket Runtime libraries. From becf9d392b9e79b95550aef63e4c8bd9299d21d4 Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Tue, 3 Oct 2023 19:40:57 +0200 Subject: [PATCH 035/256] docs(CONTRIBUTING.md): fix ordering in the dir structure section --- CONTRIBUTING.md | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e77e4a559e..d78ca49ac5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,24 +78,21 @@ These modules have native bindings to the underlying C++/Objective-C/Kotlin code capabilities to the JavaScript code. 1. [`bin`](https://github.com/socketsupply/socket/tree/master/bin): This directory contains useful scripts for building the project on different platforms, managing versions, generating documentation, publishing npm packages, etc. - 1. [`npm`](https://github.com/socketsupply/socket/tree/master/npm): This directory consists of the JavaScrip wrappers for the native code, build scripts and package directories. This directory consists of the JavaScrip wrappers for the native code, build scripts and package directories. You can also find the official Socket Runtime Node.js backend in the [`npm/packages/@socketsupply/socket-node`](https://github.com/socketsupply/socket/tree/master/npm/packages/%40socketsupply/socket-node) directory. - 1. [`src`](https://github.com/socketsupply/socket/tree/master/src): This directory contains the native code for the Socket Runtime: -- [`android`](https://github.com/socketsupply/socket/tree/master/src/android): contains the source code for the Socket Runtime library for Android -- [`app`](https://github.com/socketsupply/socket/tree/master/src/app): contains the source code related to the Socket Runtime application instance -- [`cli`](https://github.com/socketsupply/socket/tree/master/src/cli): contains the source code for the Socket Runtime CLI -- [`core`](https://github.com/socketsupply/socket/tree/master/src/core): contains the source code for the Socket Runtime core, such as Bluetooth support, File System, UDP, Peer-to-Peer capabilities, JavaScript bindings, etc. -- [`desktop`](https://github.com/socketsupply/socket/tree/master/src/desktop): contains the source code for the Socket Runtime library for desktop platforms -- [`extension`](https://github.com/socketsupply/socket/tree/master/src/extension): contains the source code for the Socket Runtime extensions ABI -- [`ios`](https://github.com/socketsupply/socket/tree/master/src/ios): contains the source code for the Socket Runtime library for iOS -- [`ipc`](https://github.com/socketsupply/socket/tree/master/src/ipc): contains the source code for the Socket Runtime IPC library -- [`process`](https://github.com/socketsupply/socket/tree/master/src/process): contains the source code for the process management -- [`window`](https://github.com/socketsupply/socket/tree/master/src/window): contains the source code for the window management on desktop platforms - + - [`android`](https://github.com/socketsupply/socket/tree/master/src/android): contains the source code for the Socket Runtime library for Android + - [`app`](https://github.com/socketsupply/socket/tree/master/src/app): contains the source code related to the Socket Runtime application instance + - [`cli`](https://github.com/socketsupply/socket/tree/master/src/cli): contains the source code for the Socket Runtime CLI + - [`core`](https://github.com/socketsupply/socket/tree/master/src/core): contains the source code for the Socket Runtime core, such as Bluetooth support, File System, UDP, Peer-to-Peer capabilities, JavaScript bindings, etc. + - [`desktop`](https://github.com/socketsupply/socket/tree/master/src/desktop): contains the source code for the Socket Runtime library for desktop platforms + - [`extension`](https://github.com/socketsupply/socket/tree/master/src/extension): contains the source code for the Socket Runtime extensions ABI + - [`ios`](https://github.com/socketsupply/socket/tree/master/src/ios): contains the source code for the Socket Runtime library for iOS + - [`ipc`](https://github.com/socketsupply/socket/tree/master/src/ipc): contains the source code for the Socket Runtime IPC library + - [`process`](https://github.com/socketsupply/socket/tree/master/src/process): contains the source code for the process management + - [`window`](https://github.com/socketsupply/socket/tree/master/src/window): contains the source code for the window management on desktop platforms 1. [`test`](https://github.com/socketsupply/socket/tree/master/src/test): This directory contains the actual Socket Runtime application that is used for testing the native code and the JavaScript API. ## Building Socket Runtime applications From 23067adfdc0bd1c9231db8c15fb2186cf446f222 Mon Sep 17 00:00:00 2001 From: Lamia <79177582+lamiazar@users.noreply.github.com> Date: Wed, 4 Oct 2023 15:28:50 -0400 Subject: [PATCH 036/256] correct typo --- api/CONFIG.md | 181 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 180 insertions(+), 1 deletion(-) diff --git a/api/CONFIG.md b/api/CONFIG.md index 27d453d867..7a6c19285b 100644 --- a/api/CONFIG.md +++ b/api/CONFIG.md @@ -1,8 +1,187 @@ # Configuration basics +The configuration file is a simple INI `socket.ini` file at the root of the project. +The file is read on startup and the values are used to configure the project. +Sometimes it's useful to # Configuration basics + The configuration file is a simple INI `socket.ini` file in the root of the project. The file is read on startup and the values are used to configure the project. -Sometimes it's useful to overide the values in `socket.ini` or keep some of the values local (e.g. `[ios] simulator_device`) +Sometimes it's useful to override the values in `socket.ini` or keep some of the values local (e.g. `[ios] simulator_device`) +or secret (e.g. `[ios] codesign_identity`, `[ios] provisioning_profile`, etc.) +This can be done by creating a file called `.ssrc` in the root of the project. + +Example: + +`socket.ini`: +```ini +; other settings + +[build] + +headless = false + +; other settings +``` + +`.ssrc`: +```ini +[build] + +headless = true + +[ios] + +codesign_identity = "iPhone Developer: John Doe (XXXXXXXXXX)" +distribution_method = "ad-hoc" +provisioning_profile = "johndoe.mobileprovision" +simulator_device = "iPhone 15" +``` + +# Section `build` + +Key | Default Value | Description +:--- | :--- | :--- +copy | "src" | ssc will copy everything in this directory to the build output directory. This is useful when you want to avoid bundling or want to use tools like vite, webpack, rollup, etc. to build your project and then copy output to the Socket bundle resources directory. +env | | An list of environment variables, separated by commas. +flags | | Advanced Compiler Settings (ie C++ compiler -02, -03, etc). +headless | false | If true, the window will never be displayed. +name | | The name of the program and executable to be output. Can't contain spaces or special characters. Required field. +output | "build" | The binary output path. It's recommended to add this path to .gitignore. +script | | The build script. It runs before the `[build] copy` phase. + +# Section `build.watch` + +Key | Default Value | Description +:--- | :--- | :--- +sources | | + +# Section `webview` + +Key | Default Value | Description +:--- | :--- | :--- +root | "/" | Make root open index.html +default_index | "" | Set default 'index.html' path to open for implicit routes +watch | false | Enable watch mode + +# Section `permissions` + +Key | Default Value | Description +:--- | :--- | :--- +allow_fullscreen | true | Allow/Disallow fullscreen in application +allow_microphone | true | Allow/Disallow microphone in application +allow_camera | true | Allow/Disallow camera in application +allow_user_media | true | Allow/Disallow user media (microphone + camera) in application +allow_geolocation | true | Allow/Disallow geolocation in application +allow_notifications | true | Allow/Disallow notifications in application +allow_sensors | true | Allow/Disallow sensors in application +allow_clipboard | true | Allow/Disallow clipboard in application +allow_bluetooth | true | Allow/Disallow bluetooth in application +allow_data_access | true | Allow/Disallow data access in application +allow_airplay | true | Allow/Disallow AirPlay access in application (macOS/iOS) only + +# Section `debug` + +Key | Default Value | Description +:--- | :--- | :--- +flags | | Advanced Compiler Settings for debug purposes (ie C++ compiler -g, etc). + +# Section `meta` + +Key | Default Value | Description +:--- | :--- | :--- +bundle_identifier | | A unique ID that identifies the bundle (used by all app stores). It's required when `[meta] type` is not `"extension"`. It should be in a reverse DNS notation https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier#discussion +copyright | | A string that gets used in the about dialog and package meta info. +description | | A short description of the app. +file_limit | | Set the limit of files that can be opened by your process. +lang | | Localization +maintainer | | A String used in the about dialog and meta info. +title | | The title of the app used in metadata files. This is NOT a window title. Can contain spaces and special characters. Defaults to name in a [build] section. +type | "" | Builds an extension when set to "extension". +version | | A string that indicates the version of the application. It should be a semver triple like 1.2.3. Defaults to 1.0.0. + +# Section `android` + +Key | Default Value | Description +:--- | :--- | :--- +aapt_no_compress | | Extensions of files that will not be stored compressed in the APK. +enable_standard_ndk_build | | Enables gradle based ndk build rather than using external native build (standard ndk is the old slow way) +main_activity | | Name of the MainActivity class. Could be overwritten by custom native code. +manifest_permissions | | Which permissions does your application need: https://developer.android.com/guide/topics/permissions/overview +native_abis | | To restrict the set of ABIs that your application supports, set them here. +native_cflags | | Used for adding custom source files and related compiler attributes. +native_sources | | +native_makefile | | +sources | | + +# Section `ios` + +Key | Default Value | Description +:--- | :--- | :--- +codesign_identity | | signing guide: https://sockets.sh/guides/#ios-1 +distribution_method | | Describes how Xcode should export the archive. Available options: app-store, package, ad-hoc, enterprise, development, and developer-id. +provisioning_profile | | A path to the provisioning profile used for signing iOS app. +simulator_device | | which device to target when building for the simulator + +# Section `linux` + +Key | Default Value | Description +:--- | :--- | :--- +categories | | Helps to make your app searchable in Linux desktop environments. +cmd | | The command to execute to spawn the "back-end" process. +icon | | The icon to use for identifying your app in Linux desktop environments. + +# Section `mac` + +Key | Default Value | Description +:--- | :--- | :--- +appstore_icon | | Mac App Store icon +category | | A category in the App Store +cmd | | The command to execute to spawn the "back-end" process. +icon | | The icon to use for identifying your app on MacOS. +sign | | TODO Signing guide: https://socketsupply.co/guides/#code-signing-certificates +codesign_identity | | +sign_paths | | + +# Section `native` + +Key | Default Value | Description +:--- | :--- | :--- +files | | Files that should be added to the compile step. +headers | | Extra Headers + +# Section `win` + +Key | Default Value | Description +:--- | :--- | :--- +cmd | | The command to execute to spawn the โ€œback-endโ€ process. +icon | | The icon to use for identifying your app on Windows. +logo | | The icon to use for identifying your app on Windows. +pfx | | A relative path to the pfx file used for signing. + +# Section `window` + +Key | Default Value | Description +:--- | :--- | :--- +height | | The initial height of the first window. +width | | The initial width of the first window. + +# Section `headless` + +Key | Default Value | Description +:--- | :--- | :--- +runner | | The headless runner command. It is used when no OS specific runner is set. +runner_flags | | The headless runner command flags. It is used when no OS specific runner is set. +runner_android | | The headless runner command for Android +runner_android_flags | | The headless runner command flags for Android +runner_ios | | The headless runner command for iOS +runner_ios_flags | | The headless runner command flags for iOS +runner_linux | | The headless runner command for Linux +runner_linux_flags | | The headless runner command flags for Linux +runner_mac | | The headless runner command for MacOS +runner_mac_flags | | The headless runner command flags for MacOS +runner_win32 | | The headless runner command for Windows +runner_win32_flags | | The headless runner command flags for Windows + the values in `socket.ini` or keep some of the values local (e.g. `[ios] simulator_device`) or secret (e.g. `[ios] codesign_identity`, `[ios] provisioning_profile`, etc.) This can be done by creating a file called `.ssrc` in the root of the project. From 138e7874021be43205c50c3ff23e583bc9de2955 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 16:35:22 -0400 Subject: [PATCH 037/256] refactor(src/cli/cli.cc): remove libraries from extension shared object being built --- src/cli/cli.cc | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 4bd2225ffc..f9f503db2b 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -4877,8 +4877,9 @@ int main (const int argc, const char* argv[]) { auto static_uv = prefixFile("lib" + d + "\\" + platform.arch + "-desktop\\libuv.lib"); auto static_runtime = trim(prefixFile("lib" + d + "\\" + platform.arch + "-desktop\\libsocket-runtime" + d + ".a")); #else - auto static_uv = prefixFile("lib/" + platform.arch + "-desktop/libuv.a"); - auto static_runtime = trim(prefixFile("lib/" + platform.arch + "-desktop/libsocket-runtime.a")); + auto d = ""; + auto static_uv = ""; + auto static_runtime = ""; #endif auto compileExtensionLibraryCommand = StringStream(); @@ -4901,8 +4902,6 @@ int main (const int argc, const char* argv[]) { #else << " " << flags << " " << extraFlags - << " -lsocket-runtime" - << " -luv" << (" -L" + quote + trim(prefixFile("lib/" + platform.arch + "-desktop")) + quote) #endif << " -fvisibility=hidden" From 97ecca938fe51ebed1dd836282b37f5efddba811 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 16:35:41 -0400 Subject: [PATCH 038/256] refactor(src/common.hh): use 'os_log_error' --- src/common.hh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/common.hh b/src/common.hh index 7a3021d176..14daecdac5 100644 --- a/src/common.hh +++ b/src/common.hh @@ -29,9 +29,8 @@ static os_log_t SSC_OS_LOG_DEBUG_BUNDLE = nullptr; } \ \ auto string = [NSString stringWithFormat: @fmt, ##__VA_ARGS__]; \ - os_log_with_type( \ + os_log_error( \ SSC_OS_LOG_DEBUG_BUNDLE, \ - OS_LOG_TYPE_ERROR, \ "%{public}s", \ string.UTF8String \ ); \ From 8014a1f9f745d5f23a41818a4f76e5e28b7c993c Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 16:36:06 -0400 Subject: [PATCH 039/256] refactor(src/ipc/ipc.hh): fix property names for latest ios/macos --- src/ipc/ipc.hh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ipc/ipc.hh b/src/ipc/ipc.hh index bf0a9be339..7c7f367a64 100644 --- a/src/ipc/ipc.hh +++ b/src/ipc/ipc.hh @@ -50,9 +50,9 @@ namespace SSC::IPC { @end @interface SSCIPCNetworkStatusObserver : NSObject -@property (strong, nonatomic) NSObject<OS_dispatch_queue>* monitorQueue; -@property (nonatomic) SSC::IPC::Router* router; -@property (retain) nw_path_monitor_t monitor; +@property (nonatomic, assign) dispatch_queue_t monitorQueue; +@property (nonatomic, assign) SSC::IPC::Router* router; +@property (nonatomic, assign) nw_path_monitor_t monitor; - (id) init; - (void) start; @end From e5d0a23417b1c3532bf6dfc22f5ad035ac445fa9 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 16:36:18 -0400 Subject: [PATCH 040/256] refactor(src/ipc/bridge.cc): capture 'this' in lambda --- src/ipc/bridge.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ipc/bridge.cc b/src/ipc/bridge.cc index e84150846d..4b2cf4012c 100644 --- a/src/ipc/bridge.cc +++ b/src/ipc/bridge.cc @@ -2488,7 +2488,7 @@ namespace SSC::IPC { #if !defined(__ANDROID__) && (defined(_WIN32) || defined(__linux__) || (defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR)) if (isDebugEnabled() && userConfig["webview_watch"] == "true") { this->fileSystemWatcher = new FileSystemWatcher(getcwd()); - this->fileSystemWatcher->start([=]( + this->fileSystemWatcher->start([=, this]( const auto& path, const auto& events, const auto& context From cda3306144c18432b8d21587fb6a003b49fa5e29 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 17:02:53 -0400 Subject: [PATCH 041/256] chore(api/CONFIG.md): generate docs --- api/CONFIG.md | 181 +------------------------------------------------- 1 file changed, 1 insertion(+), 180 deletions(-) diff --git a/api/CONFIG.md b/api/CONFIG.md index 7a6c19285b..27d453d867 100644 --- a/api/CONFIG.md +++ b/api/CONFIG.md @@ -1,187 +1,8 @@ # Configuration basics -The configuration file is a simple INI `socket.ini` file at the root of the project. -The file is read on startup and the values are used to configure the project. -Sometimes it's useful to # Configuration basics - The configuration file is a simple INI `socket.ini` file in the root of the project. The file is read on startup and the values are used to configure the project. -Sometimes it's useful to override the values in `socket.ini` or keep some of the values local (e.g. `[ios] simulator_device`) -or secret (e.g. `[ios] codesign_identity`, `[ios] provisioning_profile`, etc.) -This can be done by creating a file called `.ssrc` in the root of the project. - -Example: - -`socket.ini`: -```ini -; other settings - -[build] - -headless = false - -; other settings -``` - -`.ssrc`: -```ini -[build] - -headless = true - -[ios] - -codesign_identity = "iPhone Developer: John Doe (XXXXXXXXXX)" -distribution_method = "ad-hoc" -provisioning_profile = "johndoe.mobileprovision" -simulator_device = "iPhone 15" -``` - -# Section `build` - -Key | Default Value | Description -:--- | :--- | :--- -copy | "src" | ssc will copy everything in this directory to the build output directory. This is useful when you want to avoid bundling or want to use tools like vite, webpack, rollup, etc. to build your project and then copy output to the Socket bundle resources directory. -env | | An list of environment variables, separated by commas. -flags | | Advanced Compiler Settings (ie C++ compiler -02, -03, etc). -headless | false | If true, the window will never be displayed. -name | | The name of the program and executable to be output. Can't contain spaces or special characters. Required field. -output | "build" | The binary output path. It's recommended to add this path to .gitignore. -script | | The build script. It runs before the `[build] copy` phase. - -# Section `build.watch` - -Key | Default Value | Description -:--- | :--- | :--- -sources | | - -# Section `webview` - -Key | Default Value | Description -:--- | :--- | :--- -root | "/" | Make root open index.html -default_index | "" | Set default 'index.html' path to open for implicit routes -watch | false | Enable watch mode - -# Section `permissions` - -Key | Default Value | Description -:--- | :--- | :--- -allow_fullscreen | true | Allow/Disallow fullscreen in application -allow_microphone | true | Allow/Disallow microphone in application -allow_camera | true | Allow/Disallow camera in application -allow_user_media | true | Allow/Disallow user media (microphone + camera) in application -allow_geolocation | true | Allow/Disallow geolocation in application -allow_notifications | true | Allow/Disallow notifications in application -allow_sensors | true | Allow/Disallow sensors in application -allow_clipboard | true | Allow/Disallow clipboard in application -allow_bluetooth | true | Allow/Disallow bluetooth in application -allow_data_access | true | Allow/Disallow data access in application -allow_airplay | true | Allow/Disallow AirPlay access in application (macOS/iOS) only - -# Section `debug` - -Key | Default Value | Description -:--- | :--- | :--- -flags | | Advanced Compiler Settings for debug purposes (ie C++ compiler -g, etc). - -# Section `meta` - -Key | Default Value | Description -:--- | :--- | :--- -bundle_identifier | | A unique ID that identifies the bundle (used by all app stores). It's required when `[meta] type` is not `"extension"`. It should be in a reverse DNS notation https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier#discussion -copyright | | A string that gets used in the about dialog and package meta info. -description | | A short description of the app. -file_limit | | Set the limit of files that can be opened by your process. -lang | | Localization -maintainer | | A String used in the about dialog and meta info. -title | | The title of the app used in metadata files. This is NOT a window title. Can contain spaces and special characters. Defaults to name in a [build] section. -type | "" | Builds an extension when set to "extension". -version | | A string that indicates the version of the application. It should be a semver triple like 1.2.3. Defaults to 1.0.0. - -# Section `android` - -Key | Default Value | Description -:--- | :--- | :--- -aapt_no_compress | | Extensions of files that will not be stored compressed in the APK. -enable_standard_ndk_build | | Enables gradle based ndk build rather than using external native build (standard ndk is the old slow way) -main_activity | | Name of the MainActivity class. Could be overwritten by custom native code. -manifest_permissions | | Which permissions does your application need: https://developer.android.com/guide/topics/permissions/overview -native_abis | | To restrict the set of ABIs that your application supports, set them here. -native_cflags | | Used for adding custom source files and related compiler attributes. -native_sources | | -native_makefile | | -sources | | - -# Section `ios` - -Key | Default Value | Description -:--- | :--- | :--- -codesign_identity | | signing guide: https://sockets.sh/guides/#ios-1 -distribution_method | | Describes how Xcode should export the archive. Available options: app-store, package, ad-hoc, enterprise, development, and developer-id. -provisioning_profile | | A path to the provisioning profile used for signing iOS app. -simulator_device | | which device to target when building for the simulator - -# Section `linux` - -Key | Default Value | Description -:--- | :--- | :--- -categories | | Helps to make your app searchable in Linux desktop environments. -cmd | | The command to execute to spawn the "back-end" process. -icon | | The icon to use for identifying your app in Linux desktop environments. - -# Section `mac` - -Key | Default Value | Description -:--- | :--- | :--- -appstore_icon | | Mac App Store icon -category | | A category in the App Store -cmd | | The command to execute to spawn the "back-end" process. -icon | | The icon to use for identifying your app on MacOS. -sign | | TODO Signing guide: https://socketsupply.co/guides/#code-signing-certificates -codesign_identity | | -sign_paths | | - -# Section `native` - -Key | Default Value | Description -:--- | :--- | :--- -files | | Files that should be added to the compile step. -headers | | Extra Headers - -# Section `win` - -Key | Default Value | Description -:--- | :--- | :--- -cmd | | The command to execute to spawn the โ€œback-endโ€ process. -icon | | The icon to use for identifying your app on Windows. -logo | | The icon to use for identifying your app on Windows. -pfx | | A relative path to the pfx file used for signing. - -# Section `window` - -Key | Default Value | Description -:--- | :--- | :--- -height | | The initial height of the first window. -width | | The initial width of the first window. - -# Section `headless` - -Key | Default Value | Description -:--- | :--- | :--- -runner | | The headless runner command. It is used when no OS specific runner is set. -runner_flags | | The headless runner command flags. It is used when no OS specific runner is set. -runner_android | | The headless runner command for Android -runner_android_flags | | The headless runner command flags for Android -runner_ios | | The headless runner command for iOS -runner_ios_flags | | The headless runner command flags for iOS -runner_linux | | The headless runner command for Linux -runner_linux_flags | | The headless runner command flags for Linux -runner_mac | | The headless runner command for MacOS -runner_mac_flags | | The headless runner command flags for MacOS -runner_win32 | | The headless runner command for Windows -runner_win32_flags | | The headless runner command flags for Windows - the values in `socket.ini` or keep some of the values local (e.g. `[ios] simulator_device`) +Sometimes it's useful to overide the values in `socket.ini` or keep some of the values local (e.g. `[ios] simulator_device`) or secret (e.g. `[ios] codesign_identity`, `[ios] provisioning_profile`, etc.) This can be done by creating a file called `.ssrc` in the root of the project. From f88202037024ab18305d3460ba60ea796c7d2b4a Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 17:13:47 -0400 Subject: [PATCH 042/256] refactor(include/socket/extension.h): improve ergomics of JSON API --- include/socket/extension.h | 93 +++++++++++++++++++++++++++++++++++--- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/include/socket/extension.h b/include/socket/extension.h index 0ecbbef05b..8465e61d4f 100644 --- a/include/socket/extension.h +++ b/include/socket/extension.h @@ -406,6 +406,55 @@ extern "C" { #define sapi_json_value_is_string(value) \ (SAPI_JSON_TYPE_STRING == sapi_json_typeof((sapi_json_any_t*) (value))) + /** + * Set JSON `value` for JSON `object` at `key`. Generally, an alias to the + * `sapi_json_object_set_value` function. + * @param object - The object to set a value on + * @param key - The key of the value to set + * @param value - The JSON value to set + */ + #define sapi_json_object_set(object, key, value) \ + sapi_json_object_set_value( \ + (sapi_json_object_t*) (object), \ + (const char*)(key), \ + sapi_json_any((value)) \ + ) + + /** + * Set JSON `value` for JSON `array` at `index`. Generally, an alias to the + * `sapi_json_array_set` function. + * @param array - The array to set a value on + * @param index - The index of the value to set + * @param value - The JSON value to set + */ + #define sapi_json_array_set(array, index, value) \ + sapi_json_array_set_value( \ + (sapi_json_array_t*) (array), \ + (unsigned int) (index), \ + sapi_json_any((value)) \ + ) + + /** + * Push a JSON `value` to the end of a JSON `array`. Generally, an alias to + * the `sapi_json_array_push_value` function. + * @param array - The array to set a value on + * @param value - The JSON value to set + */ + #define sapi_json_array_push(array, value) \ + sapi_json_array_push_value ( \ + (sapi_json_array_t*) (array), \ + sapi_json_any((value)) \ + ) + + /** + * Convert JSON `value` to a string. Generally, an alias to the + * `sapi_json_string_create` function. + * @param value - The JSON value to convert to a string + * @return The JSON value as a string + */ + #define sapi_json_stringify(value) \ + sapi_json_stringify_value(sapi_json_any(value)) + /** * An opaque JSON type that represents "any" JSON value */ @@ -541,7 +590,7 @@ extern "C" { * @param value - The JSON value to set */ SOCKET_RUNTIME_EXTENSION_EXPORT - void sapi_json_object_set ( + void sapi_json_object_set_value ( sapi_json_object_t* object, const char* key, sapi_json_any_t* value @@ -566,7 +615,7 @@ extern "C" { * @param value - The JSON value to set */ SOCKET_RUNTIME_EXTENSION_EXPORT - void sapi_json_array_set ( + void sapi_json_array_set_value ( sapi_json_array_t* array, unsigned int index, sapi_json_any_t* value @@ -590,9 +639,9 @@ extern "C" { * @param value - The JSON value to set */ SOCKET_RUNTIME_EXTENSION_EXPORT - void sapi_json_array_push ( - sapi_json_array_t* json, - sapi_json_any_t* any + void sapi_json_array_push_value ( + sapi_json_array_t* array, + sapi_json_any_t* value ); /** @@ -611,7 +660,7 @@ extern "C" { * @return The JSON value as a string */ SOCKET_RUNTIME_EXTENSION_EXPORT - const char * sapi_json_stringify (const sapi_json_any_t*); + const char * sapi_json_stringify_value (const sapi_json_any_t*); /** @@ -1119,6 +1168,38 @@ extern "C" { const char* headers ); + /** + * Emit IPC `event` with `data` + * @param context - An extension context + * @param name - The event name to emit on the webview `globalThis` object + * @param data - Event data to emit + * @return `true` if successful, otherwise `false` + */ + SOCKET_RUNTIME_EXTENSION_EXPORT + bool sapi_ipc_emit ( + sapi_context_t* context, + const char* name, + const char* data + ); + + /** + * Invoke an IPC route with optional `bytes` and `size`. IPC paramters should + * be included in the URI (eg `ipc://domain.command?key=&value&key=value`) + * @param context - An extension context + * @param url - IPC URI to invoke + * @param size - Optional size of `bytes` + * @param bytes - IPC bytes to include in message + * @return `true` if successful, otherwise `false` + */ + SOCKET_RUNTIME_EXTENSION_EXPORT + bool sapi_ipc_invoke ( + sapi_context_t* context, + const char* url, + unsigned int size, + const char* bytes, + sapi_ipc_router_result_callback_t callback + ); + /** * Map a named route to a callback with optional use data for a given * extension context. Routes must "reply" with a result to respond to an From 2f8b441f852373b5c43c31fa3fa667687bf8499b Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 17:14:20 -0400 Subject: [PATCH 043/256] refactor(src/extension/json.cc): rename JSON API for use in macros --- src/extension/json.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/extension/json.cc b/src/extension/json.cc index 418e4e150b..16928f6f3b 100644 --- a/src/extension/json.cc +++ b/src/extension/json.cc @@ -50,7 +50,7 @@ sapi_json_any_t* sapi_json_raw_from ( ); } -const char * sapi_json_stringify (const sapi_json_any_t* json) { +const char * sapi_json_stringify_value (const sapi_json_any_t* json) { SSC::String string; switch (sapi_json_typeof(json)) { case SAPI_JSON_TYPE_NULL: @@ -96,7 +96,7 @@ const char * sapi_json_stringify (const sapi_json_any_t* json) { return nullptr; } -void sapi_json_object_set ( +void sapi_json_object_set_value ( sapi_json_object_t* json, const char* key, sapi_json_any_t* any @@ -140,7 +140,7 @@ sapi_json_any_t* sapi_json_object_get ( return nullptr; } -void sapi_json_array_set ( +void sapi_json_array_set_value ( sapi_json_array_t* json, unsigned int index, sapi_json_any_t* any @@ -172,7 +172,7 @@ void sapi_json_array_set ( } } -void sapi_json_array_push ( +void sapi_json_array_push_value ( sapi_json_array_t* json, sapi_json_any_t* any ) { From 4e13b918e6b209305e0bb95d1a8d8f954d89449f Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 17:15:42 -0400 Subject: [PATCH 044/256] chore(src/cli/cli.cc): include static libraries for linux too --- src/cli/cli.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index f9f503db2b..54213708a8 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -4872,7 +4872,7 @@ int main (const int argc, const char* argv[]) { } } - #if defined(_WIN32) + #if defined(_WIN32) || defined(__linux__) auto d = String(platform.win && debugBuild ? "d" : ""); auto static_uv = prefixFile("lib" + d + "\\" + platform.arch + "-desktop\\libuv.lib"); auto static_runtime = trim(prefixFile("lib" + d + "\\" + platform.arch + "-desktop\\libsocket-runtime" + d + ".a")); From 323a2f5d14914b48466ad8d645189daba802eeab Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 17:32:08 -0400 Subject: [PATCH 045/256] fix(src/cli/cli.cc): fix typo --- src/cli/cli.cc | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 54213708a8..1155d86584 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -4872,10 +4872,14 @@ int main (const int argc, const char* argv[]) { } } - #if defined(_WIN32) || defined(__linux__) - auto d = String(platform.win && debugBuild ? "d" : ""); + #if defined(_WIN32) + auto d = String(debugBuild ? "d" : ""); auto static_uv = prefixFile("lib" + d + "\\" + platform.arch + "-desktop\\libuv.lib"); auto static_runtime = trim(prefixFile("lib" + d + "\\" + platform.arch + "-desktop\\libsocket-runtime" + d + ".a")); + #elif defined(__linux__) + auto d = ""; + auto static_uv = prefixFile("lib/" + platform.arch + "-desktop/libuv.a"); + auto static_runtime = trim(prefixFile("lib/" + platform.arch + "-desktop/libsocket-runtime.a")); #else auto d = ""; auto static_uv = ""; From 402d42de5848868447b848ccd2a5627dfbf1331d Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 17:44:34 -0400 Subject: [PATCH 046/256] fix(src/cli/cli.cc): link instead on linux --- src/cli/cli.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 1155d86584..ea50a7c5f5 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -4876,10 +4876,6 @@ int main (const int argc, const char* argv[]) { auto d = String(debugBuild ? "d" : ""); auto static_uv = prefixFile("lib" + d + "\\" + platform.arch + "-desktop\\libuv.lib"); auto static_runtime = trim(prefixFile("lib" + d + "\\" + platform.arch + "-desktop\\libsocket-runtime" + d + ".a")); - #elif defined(__linux__) - auto d = ""; - auto static_uv = prefixFile("lib/" + platform.arch + "-desktop/libuv.a"); - auto static_runtime = trim(prefixFile("lib/" + platform.arch + "-desktop/libsocket-runtime.a")); #else auto d = ""; auto static_uv = ""; @@ -4906,6 +4902,10 @@ int main (const int argc, const char* argv[]) { #else << " " << flags << " " << extraFlags + #if defined(__linux__) + << " -luv" + << " -lsocket-runtime" + #endif << (" -L" + quote + trim(prefixFile("lib/" + platform.arch + "-desktop")) + quote) #endif << " -fvisibility=hidden" From e69db5ee2b85cbd45a54c1011aa9739c90b16ecf Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 15:36:51 -0400 Subject: [PATCH 047/256] chore(tsconfig.json): include more files --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 4320c55d31..68d363d7da 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["api/*.js", "api/**/*.js"], + "include": ["api/*.js", "api/**/*.js", "api/**/**/*.js"], "compilerOptions": { "baseUrl": "socket:", "target": "ES2022", From 3206508a9b82b9dd6b9bcd9a046dce0512692684 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 15:47:55 -0400 Subject: [PATCH 048/256] feat(api/enumeration.js): introduce an enumeration API --- api/enumeration.js | 104 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 api/enumeration.js diff --git a/api/enumeration.js b/api/enumeration.js new file mode 100644 index 0000000000..8691640795 --- /dev/null +++ b/api/enumeration.js @@ -0,0 +1,104 @@ +export class Enumeration extends Set { + static from (...values) { + if (values.length > 1 && typeof values[1] !== 'object') { + return new this(values) + } + + return new this(...values) + } + + constructor (values, options = {}) { + super() + + let index = options?.start ?? 0 + for (const value of values) { + if (this.has(value)) continue + + if (typeof value !== 'string' && typeof value !== 'number') { + throw new TypeError( + 'Failed to construct \'Enumeration\': ' + + 'Invalid enumerable value given.') + } + + Object.defineProperty(this, value, { + configurable: false, + enumerable: true, + writable: false, + value: index + }) + + Object.defineProperty(this, index, { + configurable: false, + enumerable: false, + writable: false, + value: value + }) + + Set.prototype.add.call(this, value) + index++ + } + + Object.freeze(this) + Object.seal(this) + } + + /** + * @ignore + * @type {string} + */ + get [Symbol.toStringTag] () { + return this.constructor.name + } + + /** + * @type {number} + */ + get length () { + return this.size + } + + /** + * @ignore + */ + add () {} + + /** + * @ignore + */ + delete () {} + + /** + * JSON represenation of a `Enumeration` instance. + * @ignore + * @return {string[]} + */ + toJSON () { + return Array.from(this) + } + + /** + * Internal inspect function. + * @ignore + * @return {LanguageQueryResult} + */ + inspect () { + const tag = this[Symbol.toStringTag] + return `${tag} { ${this.toJSON().join(', ')} }` + } + + /** + * @ignore + */ + [Symbol.for('socket.util.inspect.custom')] () { + return this.inspect() + } + + /** + * @ignore + */ + [Symbol.for('nodejs.util.inspect.custom')] () { + return this.inspect() + } +} + +export default Enumeration From 1d7b17f81fb6852f8965b96e12206ec8782b1ddb Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 15:48:17 -0400 Subject: [PATCH 049/256] test(enumeration): add 'enumeration' test --- test/src/enumeration.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 test/src/enumeration.js diff --git a/test/src/enumeration.js b/test/src/enumeration.js new file mode 100644 index 0000000000..041cec9ebd --- /dev/null +++ b/test/src/enumeration.js @@ -0,0 +1,9 @@ +import Enumeration from 'socket:enumeration' +import test from 'socket:test' + +test('Enumeration.from(values)', (t) => { + const abc = Enumeration.from(['a', 'b', 'c']) + t.ok('a' in abc, 'a in abc') + t.ok('b' in abc, 'b in abc') + t.ok('c' in abc, 'c in abc') +}) From f155319f0f0dbe8301e0befa641008f33f95b80a Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 15:49:17 -0400 Subject: [PATCH 050/256] feat(api/language): introduce 'ISO 639/RFC 5646' API --- api/language.js | 1103 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1103 insertions(+) create mode 100644 api/language.js diff --git a/api/language.js b/api/language.js new file mode 100644 index 0000000000..1669b9ae4f --- /dev/null +++ b/api/language.js @@ -0,0 +1,1103 @@ +/** + * @module Language + * + * A module for querying ISO 639-1 language names and codes and working with + * RFC 5646 language tags. + */ +import Enumeration from './enumeration.js' +import { toProperCase } from './util.js' + +/** + * Normalizes a language name to be proper cased, space or '-' separated. + * @ignore + * @param {string} value + * @return {string} + */ +function normalizeName (value) { + return value + .split(' ') + .map(toProperCase) + .join(' ') + .split('-') + .map(toProperCase) + .join('-') +} + +/** + * @ignore + * @param {string[]|Enumeration} items + * @param {string} query + * @param {boolean=} [caseSensitive = false] + * @param {boolean=} [strict = false] + * @return {{ index: number, value: string }[]} + */ +function filterItems (items, query, caseSensitive = false, strict = false) { + const filtered = [] + const escaped = query.replace('(', '\\(').replace(')', '\\)') + const regex = new RegExp( + `^(${escaped})${strict ? '$' : ''}`, + caseSensitive ? '' : 'i' + ) + + for (const item of items) { + if (regex.test(item)) { + const index = items[item] + filtered.push({ index, value: item }) + } + } + + return filtered +} + +/** + * A list of ISO 639-1 language names. + * @type {string[]} + */ +export const names = Enumeration.from([ + 'Abkhazian', + 'Afar', + 'Afrikaans', + 'Albanian', + 'Amharic', + 'Arabic', + 'Armenian', + 'Assamese', + 'Aymara', + 'Azerbaijani', + 'Bashkir', + 'Basque', + 'Bengali, Bangla', + 'Bhutani', + 'Bihari', + 'Bislama', + 'Breton', + 'Bulgarian', + 'Burmese', + 'Byelorussian', + 'Cambodian', + 'Catalan', + 'Chinese', + 'Corsican', + 'Croatian', + 'Czech', + 'Danish', + 'Dutch', + 'English, American', + 'Esperanto', + 'Estonian', + 'Faeroese', + 'Fiji', + 'Finnish', + 'French', + 'Frisian', + 'Gaelic (Scots Gaelic)', + 'Galician', + 'Georgian', + 'German', + 'Greek', + 'Greenlandic', + 'Guarani', + 'Gujarati', + 'Hausa', + 'Hebrew', + 'Hindi', + 'Hungarian', + 'Icelandic', + 'Indonesian', + 'Interlingua', + 'Interlingue', + 'Inupiak', + 'Irish', + 'Italian', + 'Japanese', + 'Javanese', + 'Kannada', + 'Kashmiri', + 'Kazakh', + 'Kinyarwanda', + 'Kirghiz', + 'Kirundi', + 'Korean', + 'Kurdish', + 'Laothian', + 'Latin', + 'Latvian, Lettish', + 'Lingala', + 'Lithuanian', + 'Macedonian', + 'Malagasy', + 'Malay', + 'Malayalam', + 'Maltese', + 'Maori', + 'Marathi', + 'Moldavian', + 'Mongolian', + 'Nauru', + 'Nepali', + 'Norwegian', + 'Occitan', + 'Oriya', + 'Oromo, Afan', + 'Pashto, Pushto', + 'Persian', + 'Polish', + 'Portuguese', + 'Punjabi', + 'Quechua', + 'Rhaeto-Romance', + 'Romanian', + 'Russian', + 'Samoan', + 'Sangro', + 'Sanskrit', + 'Serbian', + 'Serbo-Croatian', + 'Sesotho', + 'Setswana', + 'Shona', + 'Sindhi', + 'Singhalese', + 'Siswati', + 'Slovak', + 'Slovenian', + 'Somali', + 'Spanish', + 'Sudanese', + 'Swahili', + 'Swedish', + 'Tagalog', + 'Tajik', + 'Tamil', + 'Tatar', + 'Tegulu', + 'Thai', + 'Tibetan', + 'Tigrinya', + 'Tonga', + 'Tsonga', + 'Turkish', + 'Turkmen', + 'Twi', + 'Ukrainian', + 'Urdu', + 'Uzbek', + 'Vietnamese', + 'Volapuk', + 'Welsh', + 'Wolof', + 'Xhosa', + 'Yiddish', + 'Yoruba', + 'Zulu' +]) + +/** + * A list of ISO 639-1 language codes. + * @type {string[]} + */ +export const codes = Enumeration.from([ + 'AB', + 'AA', + 'AF', + 'SQ', + 'AM', + 'AR', + 'HY', + 'AS', + 'AY', + 'AZ', + 'BA', + 'EU', + 'BN', + 'DZ', + 'BH', + 'BI', + 'BR', + 'BG', + 'MY', + 'BE', + 'KM', + 'CA', + 'ZH', + 'CO', + 'HR', + 'CS', + 'DA', + 'NL', + 'EN', + 'EO', + 'ET', + 'FO', + 'FJ', + 'FI', + 'FR', + 'FY', + 'GD', + 'GL', + 'KA', + 'DE', + 'EL', + 'KL', + 'GN', + 'GU', + 'HA', + 'IW', + 'HI', + 'HU', + 'IS', + 'IN', + 'IA', + 'IE', + 'IK', + 'GA', + 'IT', + 'JA', + 'JW', + 'KN', + 'KS', + 'KK', + 'RW', + 'KY', + 'RN', + 'KO', + 'KU', + 'LO', + 'LA', + 'LV', + 'LN', + 'LT', + 'MK', + 'MG', + 'MS', + 'ML', + 'MT', + 'MI', + 'MR', + 'MO', + 'MN', + 'NA', + 'NE', + 'NO', + 'OC', + 'OR', + 'OM', + 'PS', + 'FA', + 'PL', + 'PT', + 'PA', + 'QU', + 'RM', + 'RO', + 'RU', + 'SM', + 'SG', + 'SA', + 'SR', + 'SH', + 'ST', + 'TN', + 'SN', + 'SD', + 'SI', + 'SS', + 'SK', + 'SL', + 'SO', + 'ES', + 'SU', + 'SW', + 'SV', + 'TL', + 'TG', + 'TA', + 'TT', + 'TE', + 'TH', + 'BO', + 'TI', + 'TO', + 'TS', + 'TR', + 'TK', + 'TW', + 'UK', + 'UR', + 'UZ', + 'VI', + 'VO', + 'CY', + 'WO', + 'XH', + 'JI', + 'YO', + 'ZU' +]) + +/** + * A list of RFC 5646 language tag identifiers. + * @see {@link http://tools.ietf.org/html/rfc5646} + */ +export const tags = Enumeration.from([ + 'af', + 'af-ZA', + 'ar', + 'ar-AE', + 'ar-BH', + 'ar-DZ', + 'ar-EG', + 'ar-IQ', + 'ar-JO', + 'ar-KW', + 'ar-LB', + 'ar-LY', + 'ar-MA', + 'ar-OM', + 'ar-QA', + 'ar-SA', + 'ar-SY', + 'ar-TN', + 'ar-YE', + 'az', + 'az-AZ', + 'az-Cyrl-AZ', + 'be', + 'be-BY', + 'bg', + 'bg-BG', + 'bs-BA', + 'ca', + 'ca-ES', + 'cs', + 'cs-CZ', + 'cy', + 'cy-GB', + 'da', + 'da-DK', + 'de', + 'de-AT', + 'de-CH', + 'de-DE', + 'de-LI', + 'de-LU', + 'dv', + 'dv-MV', + 'el', + 'el-GR', + 'en', + 'en-AU', + 'en-BZ', + 'en-CA', + 'en-CB', + 'en-GB', + 'en-IE', + 'en-JM', + 'en-NZ', + 'en-PH', + 'en-TT', + 'en-US', + 'en-ZA', + 'en-ZW', + 'eo', + 'es', + 'es-AR', + 'es-BO', + 'es-CL', + 'es-CO', + 'es-CR', + 'es-DO', + 'es-EC', + 'es-ES', + 'es-GT', + 'es-HN', + 'es-MX', + 'es-NI', + 'es-PA', + 'es-PE', + 'es-PR', + 'es-PY', + 'es-SV', + 'es-UY', + 'es-VE', + 'et', + 'et-EE', + 'eu', + 'eu-ES', + 'fa', + 'fa-IR', + 'fi', + 'fi-FI', + 'fo', + 'fo-FO', + 'fr', + 'fr-BE', + 'fr-CA', + 'fr-CH', + 'fr-FR', + 'fr-LU', + 'fr-MC', + 'gl', + 'gl-ES', + 'gu', + 'gu-IN', + 'he', + 'he-IL', + 'hi', + 'hi-IN', + 'hr', + 'hr-BA', + 'hr-HR', + 'hu', + 'hu-HU', + 'hy', + 'hy-AM', + 'id', + 'id-ID', + 'is', + 'is-IS', + 'it', + 'it-CH', + 'it-IT', + 'ja', + 'ja-JP', + 'ka', + 'ka-GE', + 'kk', + 'kk-KZ', + 'kn', + 'kn-IN', + 'ko', + 'ko-KR', + 'kok', + 'kok-IN', + 'ky', + 'ky-KG', + 'lt', + 'lt-LT', + 'lv', + 'lv-LV', + 'mi', + 'mi-NZ', + 'mk', + 'mk-MK', + 'mn', + 'mn-MN', + 'mr', + 'mr-IN', + 'ms', + 'ms-BN', + 'ms-MY', + 'mt', + 'mt-MT', + 'nb', + 'nb-NO', + 'nl', + 'nl-BE', + 'nl-NL', + 'nn-NO', + 'ns', + 'ns-ZA', + 'pa', + 'pa-IN', + 'pl', + 'pl-PL', + 'ps', + 'ps-AR', + 'pt', + 'pt-BR', + 'pt-PT', + 'qu', + 'qu-BO', + 'qu-EC', + 'qu-PE', + 'ro', + 'ro-RO', + 'ru', + 'ru-RU', + 'sa', + 'sa-IN', + 'se', + 'se-FI', + 'se-NO', + 'se-SE', + 'sk', + 'sk-SK', + 'sl', + 'sl-SI', + 'sq', + 'sq-AL', + 'sr-BA', + 'sr-Cyrl-BA', + 'sr-SP', + 'sr-Cyrl-SP', + 'sv', + 'sv-FI', + 'sv-SE', + 'sw', + 'sw-KE', + 'syr', + 'syr-SY', + 'ta', + 'ta-IN', + 'te', + 'te-IN', + 'th', + 'th-TH', + 'tl', + 'tl-PH', + 'tn', + 'tn-ZA', + 'tr', + 'tr-TR', + 'tt', + 'tt-RU', + 'ts', + 'uk', + 'uk-UA', + 'ur', + 'ur-PK', + 'uz', + 'uz-UZ', + 'uz-Cyrl-UZ', + 'vi', + 'vi-VN', + 'xh', + 'xh-ZA', + 'zh', + 'zh-CN', + 'zh-HK', + 'zh-MO', + 'zh-SG', + 'zh-TW', + 'zu', + 'zu-ZA', +]) + +/** + * A list of RFC 5646 language tag titles corresponding + * to language tags. + * @see {@link http://tools.ietf.org/html/rfc5646} + */ +export const descriptions = Enumeration.from([ + 'Afrikaans', + 'Afrikaans (South Africa)', + 'Arabic', + 'Arabic (U.A.E.)', + 'Arabic (Bahrain)', + 'Arabic (Algeria)', + 'Arabic (Egypt)', + 'Arabic (Iraq)', + 'Arabic (Jordan)', + 'Arabic (Kuwait)', + 'Arabic (Lebanon)', + 'Arabic (Libya)', + 'Arabic (Morocco)', + 'Arabic (Oman)', + 'Arabic (Qatar)', + 'Arabic (Saudi Arabia)', + 'Arabic (Syria)', + 'Arabic (Tunisia)', + 'Arabic (Yemen)', + 'Azeri (Latin)', + 'Azeri (Latin) (Azerbaijan)', + 'Azeri (Cyrillic) (Azerbaijan)', + 'Belarusian', + 'Belarusian (Belarus)', + 'Bulgarian', + 'Bulgarian (Bulgaria)', + 'Bosnian (Bosnia and Herzegovina)', + 'Catalan', + 'Catalan (Spain)', + 'Czech', + 'Czech (Czech Republic)', + 'Welsh', + 'Welsh (United Kingdom)', + 'Danish', + 'Danish (Denmark)', + 'German', + 'German (Austria)', + 'German (Switzerland)', + 'German (Germany)', + 'German (Liechtenstein)', + 'German (Luxembourg)', + 'Divehi', + 'Divehi (Maldives)', + 'Greek', + 'Greek (Greece)', + 'English', + 'English (Australia)', + 'English (Belize)', + 'English (Canada)', + 'English (Caribbean)', + 'English (United Kingdom)', + 'English (Ireland)', + 'English (Jamaica)', + 'English (New Zealand)', + 'English (Republic of the Philippines)', + 'English (Trinidad and Tobago)', + 'English (United States)', + 'English (South Africa)', + 'English (Zimbabwe)', + 'Esperanto', + 'Spanish', + 'Spanish (Argentina)', + 'Spanish (Bolivia)', + 'Spanish (Chile)', + 'Spanish (Colombia)', + 'Spanish (Costa Rica)', + 'Spanish (Dominican Republic)', + 'Spanish (Ecuador)', + 'Spanish (Spain)', + 'Spanish (Guatemala)', + 'Spanish (Honduras)', + 'Spanish (Mexico)', + 'Spanish (Nicaragua)', + 'Spanish (Panama)', + 'Spanish (Peru)', + 'Spanish (Puerto Rico)', + 'Spanish (Paraguay)', + 'Spanish (El Salvador)', + 'Spanish (Uruguay)', + 'Spanish (Venezuela)', + 'Estonian', + 'Estonian (Estonia)', + 'Basque', + 'Basque (Spain)', + 'Farsi', + 'Farsi (Iran)', + 'Finnish', + 'Finnish (Finland)', + 'Faroese', + 'Faroese (Faroe Islands)', + 'French', + 'French (Belgium)', + 'French (Canada)', + 'French (Switzerland)', + 'French (France)', + 'French (Luxembourg)', + 'French (Principality of Monaco)', + 'Galician', + 'Galician (Spain)', + 'Gujarati', + 'Gujarati (India)', + 'Hebrew', + 'Hebrew (Israel)', + 'Hindi', + 'Hindi (India)', + 'Croatian', + 'Croatian (Bosnia and Herzegovina)', + 'Croatian (Croatia)', + 'Hungarian', + 'Hungarian (Hungary)', + 'Armenian', + 'Armenian (Armenia)', + 'Indonesian', + 'Indonesian (Indonesia)', + 'Icelandic', + 'Icelandic (Iceland)', + 'Italian', + 'Italian (Switzerland)', + 'Italian (Italy)', + 'Japanese', + 'Japanese (Japan)', + 'Georgian', + 'Georgian (Georgia)', + 'Kazakh', + 'Kazakh (Kazakhstan)', + 'Kannada', + 'Kannada (India)', + 'Korean', + 'Korean (Korea)', + 'Konkani', + 'Konkani (India)', + 'Kyrgyz', + 'Kyrgyz (Kyrgyzstan)', + 'Lithuanian', + 'Lithuanian (Lithuania)', + 'Latvian', + 'Latvian (Latvia)', + 'Maori', + 'Maori (New Zealand)', + 'FYRO Macedonian', + 'FYRO Macedonian (Former Yugoslav Republic of Macedonia)', + 'Mongolian', + 'Mongolian (Mongolia)', + 'Marathi', + 'Marathi (India)', + 'Malay', + 'Malay (Brunei Darussalam)', + 'Malay (Malaysia)', + 'Maltese', + 'Maltese (Malta)', + 'Norwegian (Bokm?l)', + 'Norwegian (Bokm?l) (Norway)', + 'Dutch', + 'Dutch (Belgium)', + 'Dutch (Netherlands)', + 'Norwegian (Nynorsk) (Norway)', + 'Northern Sotho', + 'Northern Sotho (South Africa)', + 'Punjabi', + 'Punjabi (India)', + 'Polish', + 'Polish (Poland)', + 'Pashto', + 'Pashto (Afghanistan)', + 'Portuguese', + 'Portuguese (Brazil)', + 'Portuguese (Portugal)', + 'Quechua', + 'Quechua (Bolivia)', + 'Quechua (Ecuador)', + 'Quechua (Peru)', + 'Romanian', + 'Romanian (Romania)', + 'Russian', + 'Russian (Russia)', + 'Sanskrit', + 'Sanskrit (India)', + 'Sami', + 'Sami (Finland)', + 'Sami (Norway)', + 'Sami (Sweden)', + 'Slovak', + 'Slovak (Slovakia)', + 'Slovenian', + 'Slovenian (Slovenia)', + 'Albanian', + 'Albanian (Albania)', + 'Serbian (Latin) (Bosnia and Herzegovina)', + 'Serbian (Cyrillic) (Bosnia and Herzegovina)', + 'Serbian (Latin) (Serbia and Montenegro)', + 'Serbian (Cyrillic) (Serbia and Montenegro)', + 'Swedish', + 'Swedish (Finland)', + 'Swedish (Sweden)', + 'Swahili', + 'Swahili (Kenya)', + 'Syriac', + 'Syriac (Syria)', + 'Tamil', + 'Tamil (India)', + 'Telugu', + 'Telugu (India)', + 'Thai', + 'Thai (Thailand)', + 'Tagalog', + 'Tagalog (Philippines)', + 'Tswana', + 'Tswana (South Africa)', + 'Turkish', + 'Turkish (Turkey)', + 'Tatar', + 'Tatar (Russia)', + 'Tsonga', + 'Ukrainian', + 'Ukrainian (Ukraine)', + 'Urdu', + 'Urdu (Islamic Republic of Pakistan)', + 'Uzbek (Latin)', + 'Uzbek (Latin) (Uzbekistan)', + 'Uzbek (Cyrillic) (Uzbekistan)', + 'Vietnamese', + 'Vietnamese (Viet Nam)', + 'Xhosa', + 'Xhosa (South Africa)', + 'Chinese', + 'Chinese (S)', + 'Chinese (Hong Kong)', + 'Chinese (Macau)', + 'Chinese (Singapore)', + 'Chinese (T)', + 'Zulu', + 'Zulu (South Africa)' +]) + +/** + * A container for a language query response containing an ISO language + * name and code. + * @see {@link https://www.sitepoint.com/iso-2-letter-language-codes} + */ +export class LanguageQueryResult { + #code = null + #name = null + #tags = null + + /** + * `LanguageQueryResult` class constructor. + * @param {string} code + * @param {string} name + * @param {string[]} [tags] + */ + constructor (code, name, tags) { + this.#code = code.toUpperCase() + this.#name = normalizeName(name) + this.#tags = Enumeration.from(tags) + } + + /** + * The language code corresponding to the query. + * @type {string} + */ + get code () { + return this.#code + } + + /** + * The language name corresponding to the query. + * @type {string} + */ + get name () { + return this.#name + } + + /** + * The language tags corresponding to the query. + * @type {string[]} + */ + get tags () { + return Array.from(this.#tags) + } + + /** + * JSON represenation of a `LanguageQueryResult` instance. + * @return {{ + * code: string, + * name: string, + * tags: string[] + * }} + */ + toJSON () { + const { code, name, tags } = this + return { code, name, tags } + } + + /** + * Internal inspect function. + * @ignore + * @return {LanguageQueryResult} + */ + inspect () { + return Object.assign(new class LanguageQueryResult {}, this.toJSON()) + } + + /** + * @ignore + */ + [Symbol.for('socket.util.inspect.custom')] () { + return this.inspect() + } + + /** + * @ignore + */ + [Symbol.for('nodejs.util.inspect.custom')] () { + return this.inspect() + } +} + +/** + * A container for a language code, tag, and description. + */ +export class LanguageDescription { + #code = null + #tag = null + #description = null + + /** + * `LanguageDescription` class constructor. + * @param {string} code + * @param {string} tag + * @param {string} description + */ + constructor (code, tag, description) { + this.#code = code + this.#tag = tag + this.#description = description + } + + /** + * The language code corresponding to the language + * @type {string} + */ + get code () { + return this.#code + } + + /** + * The language tag corresponding to the language. + * @type {string} + */ + get tag () { + return this.#tag + } + + /** + * The language description corresponding to the language. + * @type {string} + */ + get description () { + return this.#description + } + + /** + * JSON represenation of a `LanguageDescription` instance. + * @return {{ + * code: string, + * tag: string, + * description: string + * }} + */ + toJSON () { + const { code, tag, description } = this + return { code, tag, description } + } + + /** + * Internal inspect function. + * @ignore + * @return {LanguageDescription} + */ + inspect () { + return Object.assign(new class LanguageDescription {}, this.toJSON()) + } + + /** + * @ignore + */ + [Symbol.for('socket.util.inspect.custom')] () { + return this.inspect() + } + + /** + * @ignore + */ + [Symbol.for('nodejs.util.inspect.custom')] () { + return this.inspect() + } +} + +/** + * Look up a language name or code by query. + * @param {string} query + * @param {object=} [options] + * @param {boolean=} [options.strict = false] + * @return {?LanguageQueryResult[]} + */ +export function lookup (query, options = { strict: false }) { + const results = [] + + if (arguments.length === 0) { + throw new TypeError( + 'Failed to lookup query:' + + '1 argument required, but only 0 present.' + ) + } + + if (typeof query !== 'string') { + throw new TypeError( + 'Failed to lookup query: Expecting string as first argument.' + ) + } + + if (query.length === 0) { + throw new TypeError( + 'Failed to lookup query: Expecting string to not be empty.' + ) + } + + if (query.length < 2) { + throw new TypeError( + 'Failed to lookup query: Expecting string to at least 2 characters.' + ) + } + + const queried = { + names: filterItems(names, query, false, options?.strict), + codes: filterItems(codes, query.toUpperCase(), true, options?.strict), + tags: filterItems(tags, query) + } + + if (queried.codes.length === 0) { + for (const tag of queried.tags) { + const [prefix] = tag.value.split('-') + queried.codes.push(...filterItems(codes, prefix)) + } + + if (queried.codes.length) { + queried.names = [] + } + } + + if (queried.codes.length === 0 && queried.names.length) { + for (const name of queried.names) { + queried.codes.push({ index: name.index, value: codes[name.index] }) + } + } + + if (queried.names.length === 0) { + for (const code of queried.codes) { + queried.names.push({ index: code.index, value: names[code.index] }) + } + } + + if (queried.tags.length === 0 && queried.codes.length) { + for (const code of queried.codes) { + queried.tags.push(...filterItems(tags, code.value)) + } + + if (queried.tags.length === 0) { + queried.tags.push(...queried.codes.map((c) => ({ value: c.value.toLowerCase() }))) + } + } + + for (const code of queried.codes) { + const name = queried.names.find((n) => n.index === code.index)?.value + + if (!name) { + continue + } + + const regex = new RegExp(`^(${code.value}-?)`, 'i') + const tags = queried.tags.filter((t) => regex.test(t.value)) + + results.push(new LanguageQueryResult( + code.value, + name, + tags.map((t) => t.value) + )) + } + + return results +} + +/** + * Describe a language by tag + * @param {string} query + * @param {object=} [options] + * @param {boolean=} [options.strict = true] + * @return {?LanguageDescription[]} + */ +export function describe (query, options = { strict: true }) { + const queried = lookup(query, options) + const results = [] + + for (const item of queried) { + for (const tag of item.tags) { + const { code } = item + const description = descriptions[tags[tag]] || names[codes[code]] + results.push(new LanguageDescription(code, tag, description)) + } + } + + return results +} + +export default { + codes, + describe, + lookup, + names, + tags +} From 6ca6ee18cbad19f575d3878109d06704c4dd51ad Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 15:49:37 -0400 Subject: [PATCH 051/256] test(language): add 'language' test --- test/src/language.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 test/src/language.js diff --git a/test/src/language.js b/test/src/language.js new file mode 100644 index 0000000000..8ee5c59322 --- /dev/null +++ b/test/src/language.js @@ -0,0 +1,32 @@ +import language from 'socket:language' +import test from 'socket:test' + +test('language.lookup(query[, options])', (t) => { + for (const name of language.names) { + const results = language.lookup(name, { strict: true }) + t.ok(results.length === 1, 'exactly 1 result in strict lookup by name: ' + name) + t.ok(results[0]?.name === name, 'result.name === name') + t.ok(results[0]?.code, 'result.code') + t.ok(results[0]?.tags.length, 'results[0].tags') + } + + for (const code of language.codes) { + const results = language.lookup(code, { strict: true }) + t.ok(results.length === 1, 'exactly 1 result in strict lookup by code: ' + code) + t.ok(results[0].code === code, 'result.code === code') + t.ok(results[0].name, 'result.name') + t.ok(results[0].tags.length, 'results[0].tags') + } +}) + +test('language.describe(tag[, options])', (t) => { + for (const tag of language.tags) { + const results = language.describe(tag, { strict: true }) + if (results.length) { + t.ok(results.length, 'results.length > 0') + t.ok(results[0]?.tag === tag, 'result.tag === ' + tag) + t.ok(results[0]?.code, 'result.code') + t.ok(results[0]?.description, 'result.description') + } + } +}) From 71c31518b56215fdff8811b5a1ebf215f5c0873c Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 15:51:39 -0400 Subject: [PATCH 052/256] feat(i18n): introduce 'i18n' API --- api/i18n.js | 215 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 api/i18n.js diff --git a/api/i18n.js b/api/i18n.js new file mode 100644 index 0000000000..54d5b43fe8 --- /dev/null +++ b/api/i18n.js @@ -0,0 +1,215 @@ +/** + * @module i18n + * Functions to internationalize your application. + * These module APIs can be used to get localized strings from locale files + * packaged with your application, find out the browser's current language, and find out the value of its Accept-Language header. + */ + +/* global XMLHttpRequest */ +import Enumeration from './enumeration.js' +import application from './application.js' +import language from './language.js' +import location from './location.js' +import hooks from './hooks.js' + +// preload environment languages when environment is ready +hooks.onReady(() => { + const languages = getUILanguage() + for (const language of languages) { + queueMicrotask(() => getMessagesForLocale(language)) + } +}) + +/** + * A cache of loaded locale messages. + * @type {Map} + */ +export const cache = new Map() + +/** + * Default location of i18n locale messages + * @type {string} + */ +export const DEFAULT_LOCALES_LOCATION = ( + application.config.i18n_locales_default || + 'i18n/locales' +) + +/** + * Get messages for `locale` pattern. This function could return many results + * for various locales given a `locale` pattern. such as `fr`, which could + * return results for `fr`, `fr-FR`, `fr-BE`, etc. + * @ignore + * @param {string} locale + * @return {object[]} + */ +export function getMessagesForLocale (locale) { + const results = [] + const tags = language.lookup(locale) + .map((l) => l.tags) + .reduce((a, t) => a.concat(t), []) + + while (tags.length) { + const tag = tags.shift() + const source = ( + application.config[`i18n_locales_${tag}_source`] || + application.config[`i18n_locales_${tag.toLowerCase()}_source`] || + application.config[`i18n_locales_${tag}`] || + application.config[`i18n_locales_${tag.toLowerCase()}`] + `${DEFAULT_LOCALES_LOCATION}/${tag}` + ) + + const path = source.endsWith('/') ? source : source + '/' + const url = String(new URL('messages.json', new URL(path, location.origin))) + + if (cache.has(url)) { + results.push(cache.get(url)) + continue + } + + const request = new XMLHttpRequest() + + try { + request.open('GET', url, false) + request.send(null) + } catch (err) { + console.warn(err.message) + continue + } + + let result = null + + // `request.responseText` can throw `InvalidStateError` error + try { + result = { + locale: tag, + // @ts-ignore + messages: JSON.parse(request.responseText) + } + } catch { + try { + result = { + locale: tag, + messages: JSON.parse(String(request.response)) + } + } catch { + continue + } + } + + if (result) { + results.push(result) + cache.set(url, result) + } + } + + return results +} + +/** + * An enumeration of supported ISO 639 language codes or RFC 5646 language tags. + * @type {Enumeration} + * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n/LanguageCode} + * @see {@link https://developer.chrome.com/docs/extensions/reference/i18n/#type-LanguageCode} + */ +export const LanguageCode = Enumeration.from(language.tags) + +/** + * Returns user preferred ISO 639 language codes or RFC 5646 language tags. + * @return {string[]} + */ +export function getAcceptLanguages () { + return globalThis.navigator?.languages ?? [] +} + +/** + * Returns the current user ISO 639 language code or RFC 5646 language tag. + * @return {?string} + */ +export function getUILanguage () { + return globalThis.navigator?.language ?? null +} + +/** + * Gets a localized message string for the specified message name. + * @param {string} messageName + * @param {object|string[]=} [substitutions = []] + * @param {object=} [options] + * @param {string=} [options.locale = null] + * @see {@link https://developer.chrome.com/docs/extensions/reference/i18n/#type-LanguageCode} + * @see {@link https://www.ibm.com/docs/en/rbd/9.5.1?topic=syslib-getmessage} + * @return {?string} + */ +export function getMessage ( + messageName, + substitutions = [], + options = { locale: null } +) { + if (typeof substitutions === 'object' && !Array.isArray(substitutions)) { + options = substitutions + } + + const locale = options?.locale ?? getUILanguage() + const results = getMessagesForLocale(locale) + + for (const result of results) { + if (messageName in result.messages) { + const { message, placeholders } = result.messages[messageName] + let interpolated = message + + for (let i = 0; i < substitutions.length; ++i) { + interpolated = interpolated + .replace(new RegExp(`\\{${i}\\}`, 'g'), substitutions[i]) + .replace(new RegExp(`\\$${i}\\$?`, 'g'), substitutions[i]) + } + + if (placeholders && typeof placeholders === 'object') { + for (const key in placeholders) { + const placeholder = placeholders[key] + let content = placeholder.content || placeholder + + // interpolate placeholder content + for (let i = 0; i < substitutions.length; ++i) { + content = content + .replace(new RegExp(`\\{${i}\\}`, 'g'), substitutions[i]) + .replace(new RegExp(`\\$${i}\\$?`, 'g'), substitutions[i]) + } + + interpolated = interpolated + .replace(new RegExp(`\\{${key}\\}`, 'gi'), content) + .replace(new RegExp(`\\$${key}\\$?`, 'gi'), content) + } + } + + return interpolated + } + } + + return null +} + +/** + * Gets a localized message description string for the specified message name. + * @param {string} messageName + * @return {?string} + */ +export function getMessageDescription (messageName) { + const locale = options?.locale ?? getUILanguage() + const results = getMessagesForLocale(locale) + + for (const result of results) { + if (messageName in result.messages) { + const { description } = result.messages[messageName] + return description || null + } + } + + return null +} + +export default { + LanguageCode, + getAcceptLanguages, + getMessage, + getUILanguage +} From b725ab96938c250d3309b7e1aa0064e72cfabffd Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 15:57:51 -0400 Subject: [PATCH 053/256] test(i18n): add 'i18n' tests --- test/src/i18n.js | 44 +++++++++++++++++++++++ test/src/i18n/locales/en/messages.json | 23 ++++++++++++ test/src/i18n/locales/fr-FR/messages.json | 23 ++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 test/src/i18n.js create mode 100644 test/src/i18n/locales/en/messages.json create mode 100644 test/src/i18n/locales/fr-FR/messages.json diff --git a/test/src/i18n.js b/test/src/i18n.js new file mode 100644 index 0000000000..8f4f5f4393 --- /dev/null +++ b/test/src/i18n.js @@ -0,0 +1,44 @@ +import test from 'socket:test' +import i18n from 'socket:i18n' + +test('i18n.getMessage(messageName[,substitutions = [][, options]])', (t) => { + const en = { + key1: i18n.getMessage('key1', { locale: 'en' }), + key2: i18n.getMessage('key2', { locale: 'en' }), + 'key-with-indexed-substitutions': i18n.getMessage( + 'key-with-indexed-substitutions', + ['first', 'second', 'third'], + { locale: 'en' } + ), + 'key-with-placeholder-substitutions': i18n.getMessage( + 'key-with-placeholder-substitutions', + ['first', 'second', 'third'], + { locale: 'en' } + ) + } + + const fr = { + key1: i18n.getMessage('key1', { locale: 'fr' }), + key2: i18n.getMessage('key2', { locale: 'fr-fr' }), + 'key-with-indexed-substitutions': i18n.getMessage( + 'key-with-indexed-substitutions', + ['premier!', 'deuxiรจme!', 'troisiรจme!'], + { locale: 'fr' } + ), + 'key-with-placeholder-substitutions': i18n.getMessage( + 'key-with-placeholder-substitutions', + ['premier!', 'deuxiรจme!', 'troisiรจme!'], + { locale: 'fr-FR' } + ) + } + + t.equal(en.key1, 'A message for key1', 'en.key1') + t.equal(en.key2, 'A message for key2', 'en.key2') + t.equal(en['key-with-indexed-substitutions'], 'first=first, second=second, third=third', 'en[key-with-indexed-substitutions]') + t.equal(en['key-with-placeholder-substitutions'], 'first=first, second=second, third=third', 'en[key-with-placeholder-substitutions]') + + t.equal(fr.key1, 'Un message pour la clรฉย 1', 'fr.key1') + t.equal(fr.key2, 'Un message pour la clรฉย 2', 'fr.key2') + t.equal(fr['key-with-indexed-substitutions'], 'premier=premier!, deuxiรจme=deuxiรจme!, troisiรจme=troisiรจme!', 'fr[key-with-indexed-substitutions]') + t.equal(fr['key-with-placeholder-substitutions'], 'premier=premier!, deuxiรจme=deuxiรจme!, troisiรจme=troisiรจme!', 'fr[key-with-placeholder-substitutions]') +}) diff --git a/test/src/i18n/locales/en/messages.json b/test/src/i18n/locales/en/messages.json new file mode 100644 index 0000000000..147ba061c1 --- /dev/null +++ b/test/src/i18n/locales/en/messages.json @@ -0,0 +1,23 @@ +{ + "key1": { + "message": "A message for key1", + "description": "A message for key1" + }, + "key2": { + "message": "A message for key2", + "description": "A message for key2" + }, + "key-with-indexed-substitutions": { + "message": "first={0}, second=$1, third={2}", + "description": "A message for 3 substitutions" + }, + "key-with-placeholder-substitutions": { + "message": "first={first}, second=$second, third=$THIRD$", + "description": "A message for 3 substitutions", + "placeholders": { + "first": { "content": "$0" }, + "second": { "content": "{1}" }, + "third": { "content": "$2$" } + } + } +} diff --git a/test/src/i18n/locales/fr-FR/messages.json b/test/src/i18n/locales/fr-FR/messages.json new file mode 100644 index 0000000000..5d5ac2cbd9 --- /dev/null +++ b/test/src/i18n/locales/fr-FR/messages.json @@ -0,0 +1,23 @@ +{ + "key1": { + "message": "Un message pour la clรฉย 1", + "description": "A message for key1" + }, + "key2": { + "message": "Un message pour la clรฉย 2", + "description": "A message for key2" + }, + "key-with-indexed-substitutions": { + "message": "premier={0}, deuxiรจme=$1, troisiรจme={2}", + "description": "A message for 3 substitutions" + }, + "key-with-placeholder-substitutions": { + "message": "premier={first}, deuxiรจme=$second$, troisiรจme=$THIRD$", + "description": "A message for 3 substitutions", + "placeholders": { + "first": { "content": "$0" }, + "second": { "content": "{1}" }, + "third": { "content": "$2$" } + } + } +} From fd384a8ff80d054fbc0f18cb40569acd427b0815 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 15:58:15 -0400 Subject: [PATCH 054/256] chore(test/socket.ini): configure 'i18n' test locales --- test/socket.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/socket.ini b/test/socket.ini index 595f41787f..bf270ae4d0 100644 --- a/test/socket.ini +++ b/test/socket.ini @@ -36,6 +36,11 @@ copyright = "Socket Supply Co. ยฉ 2021-2022" maintainer = "Socket Supply Co." bundle_identifier = co.socketsupply.socket.tests +[i18n.locales] +en = i18n/locales/en +fr = i18n/locales/fr-FR +fr-FR = i18n/locales/fr-FR + [debug] flags = -g From 68098e9be38d78591ca9bc93b6be38d594a82341 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 17:18:02 -0400 Subject: [PATCH 055/256] chore(api/index.d.ts): generate types --- api/index.d.ts | 238 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) diff --git a/api/index.d.ts b/api/index.d.ts index b829db5cda..e4a356afae 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -3990,6 +3990,37 @@ declare module "socket:dgram" { import { InternalError } from "socket:errors"; } +declare module "socket:enumeration" { + export class Enumeration extends Set<any> { + static from(...values: any[]): Enumeration; + constructor(values: any, options?: {}); + /** + * @type {number} + */ + get length(): number; + /** + * @ignore + */ + add(): void; + /** + * @ignore + */ + delete(): void; + /** + * JSON represenation of a `Enumeration` instance. + * @ignore + * @return {string[]} + */ + toJSON(): string[]; + /** + * Internal inspect function. + * @ignore + * @return {LanguageQueryResult} + */ + inspect(): LanguageQueryResult; + } + export default Enumeration; +} declare module "socket:extension" { /** * Load an extension by name. @@ -4296,6 +4327,213 @@ declare module "socket:hooks" { const _default: Hooks; export default _default; } +declare module "socket:language" { + /** + * Look up a language name or code by query. + * @param {string} query + * @param {object=} [options] + * @param {boolean=} [options.strict = false] + * @return {?LanguageQueryResult[]} + */ + export function lookup(query: string, options?: object | undefined, ...args: any[]): LanguageQueryResult[] | null; + /** + * Describe a language by tag + * @param {string} query + * @param {object=} [options] + * @param {boolean=} [options.strict = true] + * @return {?LanguageDescription[]} + */ + export function describe(query: string, options?: object | undefined): LanguageDescription[] | null; + /** + * A list of ISO 639-1 language names. + * @type {string[]} + */ + export const names: string[]; + /** + * A list of ISO 639-1 language codes. + * @type {string[]} + */ + export const codes: string[]; + /** + * A list of RFC 5646 language tag identifiers. + * @see {@link http://tools.ietf.org/html/rfc5646} + */ + export const tags: Enumeration; + /** + * A list of RFC 5646 language tag titles corresponding + * to language tags. + * @see {@link http://tools.ietf.org/html/rfc5646} + */ + export const descriptions: Enumeration; + /** + * A container for a language query response containing an ISO language + * name and code. + * @see {@link https://www.sitepoint.com/iso-2-letter-language-codes} + */ + export class LanguageQueryResult { + /** + * `LanguageQueryResult` class constructor. + * @param {string} code + * @param {string} name + * @param {string[]} [tags] + */ + constructor(code: string, name: string, tags?: string[]); + /** + * The language code corresponding to the query. + * @type {string} + */ + get code(): string; + /** + * The language name corresponding to the query. + * @type {string} + */ + get name(): string; + /** + * The language tags corresponding to the query. + * @type {string[]} + */ + get tags(): string[]; + /** + * JSON represenation of a `LanguageQueryResult` instance. + * @return {{ + * code: string, + * name: string, + * tags: string[] + * }} + */ + toJSON(): { + code: string; + name: string; + tags: string[]; + }; + /** + * Internal inspect function. + * @ignore + * @return {LanguageQueryResult} + */ + inspect(): LanguageQueryResult; + #private; + } + /** + * A container for a language code, tag, and description. + */ + export class LanguageDescription { + /** + * `LanguageDescription` class constructor. + * @param {string} code + * @param {string} tag + * @param {string} description + */ + constructor(code: string, tag: string, description: string); + /** + * The language code corresponding to the language + * @type {string} + */ + get code(): string; + /** + * The language tag corresponding to the language. + * @type {string} + */ + get tag(): string; + /** + * The language description corresponding to the language. + * @type {string} + */ + get description(): string; + /** + * JSON represenation of a `LanguageDescription` instance. + * @return {{ + * code: string, + * tag: string, + * description: string + * }} + */ + toJSON(): { + code: string; + tag: string; + description: string; + }; + /** + * Internal inspect function. + * @ignore + * @return {LanguageDescription} + */ + inspect(): LanguageDescription; + #private; + } + namespace _default { + export { codes }; + export { describe }; + export { lookup }; + export { names }; + export { tags }; + } + export default _default; + import Enumeration from "socket:enumeration"; +} +declare module "socket:i18n" { + /** + * Get messages for `locale` pattern. This function could return many results + * for various locales given a `locale` pattern. such as `fr`, which could + * return results for `fr`, `fr-FR`, `fr-BE`, etc. + * @ignore + * @param {string} locale + * @return {object[]} + */ + export function getMessagesForLocale(locale: string): object[]; + /** + * Returns user preferred ISO 639 language codes or RFC 5646 language tags. + * @return {string[]} + */ + export function getAcceptLanguages(): string[]; + /** + * Returns the current user ISO 639 language code or RFC 5646 language tag. + * @return {?string} + */ + export function getUILanguage(): string | null; + /** + * Gets a localized message string for the specified message name. + * @param {string} messageName + * @param {object|string[]=} [substitutions = []] + * @param {object=} [options] + * @param {string=} [options.locale = null] + * @see {@link https://developer.chrome.com/docs/extensions/reference/i18n/#type-LanguageCode} + * @see {@link https://www.ibm.com/docs/en/rbd/9.5.1?topic=syslib-getmessage} + * @return {?string} + */ + export function getMessage(messageName: string, substitutions?: (object | string[]) | undefined, options?: object | undefined): string | null; + /** + * Gets a localized message description string for the specified message name. + * @param {string} messageName + * @return {?string} + */ + export function getMessageDescription(messageName: string): string | null; + /** + * A cache of loaded locale messages. + * @type {Map} + */ + export const cache: Map<any, any>; + /** + * Default location of i18n locale messages + * @type {string} + */ + export const DEFAULT_LOCALES_LOCATION: string; + /** + * An enumeration of supported ISO 639 language codes or RFC 5646 language tags. + * @type {Enumeration} + * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n/LanguageCode} + * @see {@link https://developer.chrome.com/docs/extensions/reference/i18n/#type-LanguageCode} + */ + export const LanguageCode: Enumeration; + namespace _default { + export { LanguageCode }; + export { getAcceptLanguages }; + export { getMessage }; + export { getUILanguage }; + } + export default _default; + import Enumeration from "socket:enumeration"; +} declare module "socket:test/fast-deep-equal" { export default function equal(a: any, b: any): boolean; } From a22f3841e9824202473cc7c9dac8d01f8d18ee4c Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 17:27:36 -0400 Subject: [PATCH 056/256] refactor(api/enumeration.js): docs and 'Enumeration#contains' --- api/enumeration.js | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/api/enumeration.js b/api/enumeration.js index 8691640795..8c25b6ddbc 100644 --- a/api/enumeration.js +++ b/api/enumeration.js @@ -1,4 +1,18 @@ +/** + * @module enumeration + * This module provides a data structure for enumerated unique values. + */ + +/** + * A container for enumerated values. + */ export class Enumeration extends Set { + + /** + * Creates an `Enumeration` instance from arguments. + * @param {...any} values + * @return {Enumeration} + */ static from (...values) { if (values.length > 1 && typeof values[1] !== 'object') { return new this(values) @@ -7,7 +21,13 @@ export class Enumeration extends Set { return new this(...values) } - constructor (values, options = {}) { + /** + * `Enumeration` class constructor. + * @param {any[]} values + * @param {object=} [options = {}] + * @param {number=} [options.start = 0] + */ + constructor (values, options = { start: 0 }) { super() let index = options?.start ?? 0 @@ -31,7 +51,7 @@ export class Enumeration extends Set { configurable: false, enumerable: false, writable: false, - value: value + value }) Set.prototype.add.call(this, value) @@ -57,6 +77,15 @@ export class Enumeration extends Set { return this.size } + /** + * Returns `true` if enumeration contains `value`. An alias + * for `Set.prototype.has`. + * @return {boolean} + */ + contains (value) { + return this.has(value) + } + /** * @ignore */ From 97d0b100b98bd2dbecb22465cbc676629eee3628 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 17:27:47 -0400 Subject: [PATCH 057/256] refactor(api/i18n.js): clean up --- api/i18n.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/api/i18n.js b/api/i18n.js index 54d5b43fe8..4998f17b19 100644 --- a/api/i18n.js +++ b/api/i18n.js @@ -55,7 +55,7 @@ export function getMessagesForLocale (locale) { application.config[`i18n_locales_${tag}_source`] || application.config[`i18n_locales_${tag.toLowerCase()}_source`] || application.config[`i18n_locales_${tag}`] || - application.config[`i18n_locales_${tag.toLowerCase()}`] + application.config[`i18n_locales_${tag.toLowerCase()}`] || `${DEFAULT_LOCALES_LOCATION}/${tag}` ) @@ -191,9 +191,14 @@ export function getMessage ( /** * Gets a localized message description string for the specified message name. * @param {string} messageName + * @param {object=} [options] + * @param {string=} [options.locale = null] * @return {?string} */ -export function getMessageDescription (messageName) { +export function getMessageDescription ( + messageName, + options = { locale: null } +) { const locale = options?.locale ?? getUILanguage() const results = getMessagesForLocale(locale) From c63153b2167b16dd8e4a7a0f1ad60b4ea0da3728 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 17:27:59 -0400 Subject: [PATCH 058/256] chore(api/language.js): lint --- api/language.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/language.js b/api/language.js index 1669b9ae4f..331c1502bd 100644 --- a/api/language.js +++ b/api/language.js @@ -571,7 +571,7 @@ export const tags = Enumeration.from([ 'zh-SG', 'zh-TW', 'zu', - 'zu-ZA', + 'zu-ZA' ]) /** @@ -879,6 +879,7 @@ export class LanguageQueryResult { * @return {LanguageQueryResult} */ inspect () { + // eslint-disable-next-line return Object.assign(new class LanguageQueryResult {}, this.toJSON()) } @@ -960,6 +961,7 @@ export class LanguageDescription { * @return {LanguageDescription} */ inspect () { + // eslint-disable-next-line return Object.assign(new class LanguageDescription {}, this.toJSON()) } @@ -1080,7 +1082,7 @@ export function lookup (query, options = { strict: false }) { * @return {?LanguageDescription[]} */ export function describe (query, options = { strict: true }) { - const queried = lookup(query, options) + const queried = lookup(query, options) const results = [] for (const item of queried) { From c3067ef58636995e597a1c695fa961667a37c73f Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 17:28:27 -0400 Subject: [PATCH 059/256] chore(api): generate types --- api/enumeration.js | 1 - api/index.d.ts | 30 ++++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/api/enumeration.js b/api/enumeration.js index 8c25b6ddbc..f236e94133 100644 --- a/api/enumeration.js +++ b/api/enumeration.js @@ -7,7 +7,6 @@ * A container for enumerated values. */ export class Enumeration extends Set { - /** * Creates an `Enumeration` instance from arguments. * @param {...any} values diff --git a/api/index.d.ts b/api/index.d.ts index e4a356afae..dbe4ecd401 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -3991,13 +3991,37 @@ declare module "socket:dgram" { } declare module "socket:enumeration" { + /** + * @module enumeration + * This module provides a data structure for enumerated unique values. + */ + /** + * A container for enumerated values. + */ export class Enumeration extends Set<any> { + /** + * Creates an `Enumeration` instance from arguments. + * @param {...any} values + * @return {Enumeration} + */ static from(...values: any[]): Enumeration; - constructor(values: any, options?: {}); + /** + * `Enumeration` class constructor. + * @param {any[]} values + * @param {object=} [options = {}] + * @param {number=} [options.start = 0] + */ + constructor(values: any[], options?: object | undefined); /** * @type {number} */ get length(): number; + /** + * Returns `true` if enumeration contains `value`. An alias + * for `Set.prototype.has`. + * @return {boolean} + */ + contains(value: any): boolean; /** * @ignore */ @@ -4505,9 +4529,11 @@ declare module "socket:i18n" { /** * Gets a localized message description string for the specified message name. * @param {string} messageName + * @param {object=} [options] + * @param {string=} [options.locale = null] * @return {?string} */ - export function getMessageDescription(messageName: string): string | null; + export function getMessageDescription(messageName: string, options?: object | undefined): string | null; /** * A cache of loaded locale messages. * @type {Map} From b8ce63b3fdbefa9498d72bce4208db332433ab86 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 18:01:13 -0400 Subject: [PATCH 060/256] feat(api/hooks.js): introduce 'onLanguageChange' hook --- api/hooks.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/api/hooks.js b/api/hooks.js index 2400da6f10..4a8081760d 100644 --- a/api/hooks.js +++ b/api/hooks.js @@ -368,6 +368,16 @@ export class Hooks extends EventTarget { this.addEventListener('offline', callback) return () => this.removeEventListener('offline', callback) } + + /** + * Calls callback when runtime user preferred language has changed. + * @param {function} callback + * @return {function} + */ + onLanguageChange (callback) { + this.addEventListener('languagechange', callback) + return () => this.removeEventListener('languagechange', callback) + } } export default new Hooks() From 775a0492ad56458abef84bc8ecc99582ac94aaf2 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 18:02:02 -0400 Subject: [PATCH 061/256] refactor(api/i18n): preload on ready and when language changes --- api/i18n.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/api/i18n.js b/api/i18n.js index 4998f17b19..21b5b22fc0 100644 --- a/api/i18n.js +++ b/api/i18n.js @@ -12,13 +12,20 @@ import language from './language.js' import location from './location.js' import hooks from './hooks.js' -// preload environment languages when environment is ready -hooks.onReady(() => { - const languages = getUILanguage() - for (const language of languages) { +// preload environment languages when environment is ready and listen +// for language change events +hooks.onReady(preloadUILanguage) +hooks.onLanguageChange(preloadUILanguage) + +/** + * Preloads current UI language into cache. + * @ignore + */ +function preloadUILanguage () { + for (const language of getAcceptLanguages()) { queueMicrotask(() => getMessagesForLocale(language)) } -}) +} /** * A cache of loaded locale messages. From 37de67ea071f636819a3dd8a6a74e68093230af1 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 18:02:20 -0400 Subject: [PATCH 062/256] test(): include 'language' and 'i18n' tests --- test/src/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/src/index.js b/test/src/index.js index 87d89f5c2a..e3c368acf5 100644 --- a/test/src/index.js +++ b/test/src/index.js @@ -18,3 +18,6 @@ import './commonjs.js' import './extension.js' import './url.js' import './test.js' +import './enumeration.js' +import './language.js' +import './i18n.js' From bc56a839d1535191dbfae21858049d82abc114f7 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 18:03:53 -0400 Subject: [PATCH 063/256] chore(api): generate types --- api/index.d.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/index.d.ts b/api/index.d.ts index dbe4ecd401..168250912a 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -4346,6 +4346,12 @@ declare module "socket:hooks" { * @return {function} */ onOffline(callback: Function): Function; + /** + * Calls callback when runtime user preferred language has changed. + * @param {function} callback + * @return {function} + */ + onLanguageChange(callback: Function): Function; #private; } const _default: Hooks; From 00d1d761937397040101a9087cdadf400a309200 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Wed, 4 Oct 2023 21:34:19 -0400 Subject: [PATCH 064/256] fix(types): fixes to @socketsupply/socket types --- api/application.js | 4 ++-- api/extension.js | 19 +++++++++++++------ api/index.d.ts | 39 ++++++++++++++++++++++++--------------- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/api/application.js b/api/application.js index db1ec63dc3..718e8585d2 100644 --- a/api/application.js +++ b/api/application.js @@ -108,7 +108,7 @@ function throwOnInvalidIndex (index) { /** * Returns the ApplicationWindow instances for the given indices or all windows if no indices are provided. - * @param {number[]|undefined} indices - the indices of the windows + * @param {number[]} [indices] - the indices of the windows * @return {Promise<Object.<number, ApplicationWindow>>} * @throws {Error} - if indices is not an array of integer numbers */ @@ -147,7 +147,7 @@ export async function getCurrentWindow () { /** * Quits the backend process and then quits the render process, the exit code used is the final exit code to the OS. - * @param {object} code - an exit code + * @param {number} [code] - an exit code * @return {Promise<ipc.Result>} */ export async function exit (code) { diff --git a/api/extension.js b/api/extension.js index 2604c64661..bf619c260e 100644 --- a/api/extension.js +++ b/api/extension.js @@ -21,13 +21,15 @@ const $loaded = Symbol('loaded') /** * A interface for a native extension. + * @template {Record<string, any> T} */ export class Extension extends EventTarget { /** * Load an extension by name. + * @template {Record<string, any> T} * @param {string} name - * @param {{ allow: string[] | string ?} [options] - * @return {Promise<Extension>} + * @param {ExtensionLoadOptions} [options] + * @return {Promise<Extension<T>>} */ static async load (name, options) { options = { name, ...options } @@ -86,7 +88,7 @@ export class Extension extends EventTarget { options = {} /** - * @type {Proxy} + * @type {T} */ binding = null @@ -94,7 +96,7 @@ export class Extension extends EventTarget { * `Extension` class constructor. * @param {string} name * @param {ExtensionInfo} info - * @param {object?} [options] + * @param {ExtensionLoadOptions} [options] */ constructor (name, info, options = null) { super() @@ -144,11 +146,16 @@ export class Extension extends EventTarget { } } +/** + * @typedef {{ allow: string[] | string }} ExtensionLoadOptions + */ + /** * Load an extension by name. + * @template {Record<string, any> T} * @param {string} name - * @param {object?} [options] - * @return {Promise<Extension>} + * @param {ExtensionLoadOptions} [options] + * @return {Promise<Extension<T>>} */ export async function load (name, options = {}) { return await Extension.load(name, options) diff --git a/api/index.d.ts b/api/index.d.ts index 168250912a..66dc46e618 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -3388,11 +3388,11 @@ declare module "socket:application" { export function getScreenSize(): Promise<ipc.Result>; /** * Returns the ApplicationWindow instances for the given indices or all windows if no indices are provided. - * @param {number[]|undefined} indices - the indices of the windows + * @param {number[]} [indices] - the indices of the windows * @return {Promise<Object.<number, ApplicationWindow>>} * @throws {Error} - if indices is not an array of integer numbers */ - export function getWindows(indices: number[] | undefined): Promise<{ + export function getWindows(indices?: number[]): Promise<{ [x: number]: ApplicationWindow; }>; /** @@ -3409,10 +3409,10 @@ declare module "socket:application" { export function getCurrentWindow(): Promise<ApplicationWindow>; /** * Quits the backend process and then quits the render process, the exit code used is the final exit code to the OS. - * @param {object} code - an exit code + * @param {number} [code] - an exit code * @return {Promise<ipc.Result>} */ - export function exit(code: object): Promise<ipc.Result>; + export function exit(code?: number): Promise<ipc.Result>; /** * Set the native menu for the app. * @@ -4046,13 +4046,17 @@ declare module "socket:enumeration" { export default Enumeration; } declare module "socket:extension" { + /** + * @typedef {{ allow: string[] | string }} ExtensionLoadOptions + */ /** * Load an extension by name. + * @template {Record<string, any> T} * @param {string} name - * @param {object?} [options] - * @return {Promise<Extension>} + * @param {ExtensionLoadOptions} [options] + * @return {Promise<Extension<T>>} */ - export function load(name: string, options?: object | null): Promise<Extension>; + export function load<T extends Record<string, any>>(name: string, options?: ExtensionLoadOptions): Promise<Extension<T>>; /** * Provides current stats about the loaded extensions. * @return {Promise<ExtensionStats>} @@ -4066,15 +4070,17 @@ declare module "socket:extension" { */ /** * A interface for a native extension. + * @template {Record<string, any> T} */ - export class Extension extends EventTarget { + export class Extension<T extends Record<string, any>> extends EventTarget { /** * Load an extension by name. + * @template {Record<string, any> T} * @param {string} name - * @param {{ allow: string[] | string ?} [options] - * @return {Promise<Extension>} + * @param {ExtensionLoadOptions} [options] + * @return {Promise<Extension<T>>} */ - static load(name: string, options: any): Promise<Extension>; + static load<T_1 extends Record<string, any>>(name: string, options?: ExtensionLoadOptions): Promise<Extension<T_1>>; /** * Provides current stats about the loaded extensions. * @return {Promise<ExtensionStats>} @@ -4084,9 +4090,9 @@ declare module "socket:extension" { * `Extension` class constructor. * @param {string} name * @param {ExtensionInfo} info - * @param {object?} [options] + * @param {ExtensionLoadOptions} [options] */ - constructor(name: string, info: ExtensionInfo, options?: object | null); + constructor(name: string, info: ExtensionInfo, options?: ExtensionLoadOptions); /** * The name of the extension * @type {string?} @@ -4112,9 +4118,9 @@ declare module "socket:extension" { */ options: object; /** - * @type {Proxy} + * @type {T} */ - binding: ProxyConstructor; + binding: T; /** * `true` if the extension was loaded, otherwise `false` * @type {boolean} @@ -4132,6 +4138,9 @@ declare module "socket:extension" { export { stats }; } export default _default; + export type ExtensionLoadOptions = { + allow: string[] | string; + }; export type ExtensionInfo = { abi: number; version: string; From c99b4ad55e6fbb04da387d08c20a82747d4ef5d0 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:50:00 -0400 Subject: [PATCH 065/256] fix(docs-generator): handle optional param without default value --- bin/docs-generator/api-module.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/docs-generator/api-module.js b/bin/docs-generator/api-module.js index 3987b6702f..e081f4416c 100644 --- a/bin/docs-generator/api-module.js +++ b/bin/docs-generator/api-module.js @@ -141,21 +141,21 @@ export function generateApiModuleDoc ({ item.signature = item.signature || [] const parts = attr.split(/-\s+(.*)/) const { 1: rawType, 2: rawName } = parts[0].match(/{([^}]+)}(.*)/) - const [name, defaultValue] = rawName.replace(/[[\]']+/g, '').trim().split('=') + const [name, defaultValue] = rawName.replace(/[[\]']+/g, '').trim().split(/ *[=] */) // type could be [(string|number)=] const parenthasisedType = rawType .replace(/\s*\|\s*/g, ' \\| ') .replace(/\[|\]/g, '') // now it is (string|number)= - const optional = parenthasisedType.endsWith('=') + const optional = parenthasisedType.endsWith('=') || /^ *\[/.test(rawName) const compundType = parenthasisedType.replace(/=$/, '') // now it is (string|number) const type = compundType.match(/^\((.*)\)$/)?.[1] ?? compundType // now it is string|number const param = { - name: name.trim() || `(Position ${position++})`, + name: name || `(Position ${position++})`, type } From f008e6d985326bf15b5313cafac992cc1535b4e5 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:50:07 -0400 Subject: [PATCH 066/256] chore: update api docs --- api/README.md | 190 ++++++++++++++++++++++----------------------- api/application.js | 4 +- api/index.d.ts | 2 +- 3 files changed, 98 insertions(+), 98 deletions(-) diff --git a/api/README.md b/api/README.md index 8f6a8d25f9..9376770bb1 100644 --- a/api/README.md +++ b/api/README.md @@ -47,7 +47,7 @@ Returns the ApplicationWindow instances for the given indices or all windows if | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | -| indices | number \| undefined | | false | the indices of the windows | +| indices | number | | true | the indices of the windows | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -79,7 +79,7 @@ Quits the backend process and then quits the render process, the exit code used | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | -| code | object | | false | an exit code | +| code | number | 0 | true | an exit code | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -215,7 +215,7 @@ The application's backend instance. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | opts | object | | false | an options object | -| opts.force | boolean | false | false | whether to force the existing process to close | +| opts.force | boolean | false | true | whether to force the existing process to close | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -261,7 +261,7 @@ Start the Bluetooth service. | :--- | :--- | :--- | | Not specified | Promise<ipc.Result> | | -### [`subscribe(id )`](https://github.com/socketsupply/socket/blob/master/api/bluetooth.js#L119) +### [`subscribe(id)`](https://github.com/socketsupply/socket/blob/master/api/bluetooth.js#L119) Start scanning for published values that correspond to a well-known UUID. Once subscribed to a UUID, events that correspond to that UUID will be @@ -278,7 +278,7 @@ Start scanning for published values that correspond to a well-known UUID. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | -| id | string | | false | A well-known UUID | +| id | string | | true | A well-known UUID | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -290,8 +290,8 @@ Start advertising a new value for a well-known UUID | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | -| id | string | | false | A well-known UUID | -| value | string | | false | | +| id | string | | true | A well-known UUID | +| value | string | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -733,7 +733,7 @@ External docs: https://nodejs.org/api/events.html import * as fs from 'socket:fs'; ``` -## [`access(path, mode , callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L84) +## [`access(path, mode, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L84) External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsopenpath-flags-mode-callback Asynchronously check access a file for a given mode calling `callback` @@ -742,8 +742,8 @@ Asynchronously check access a file for a given mode calling `callback` | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | path | string \| Buffer \| URL | | false | | -| mode | string? \| function(Error?)? | F_OK(0) | false | | -| callback | function(Error?)? | | false | | +| mode | string? \| function(Error?)? | F_OK(0) | true | | +| callback | function(Error?)? | | true | | ## [`chmod(path, mode, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L117) @@ -767,7 +767,7 @@ Asynchronously close a file descriptor calling `callback` upon success or error. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | fd | number | | false | | -| callback | function(Error?)? | | false | | +| callback | function(Error?)? | | true | | ## [`copyFile(src, dest, flags, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L190) @@ -789,7 +789,7 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscreatewri | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | path | string \| Buffer \| URL | | false | | -| options | object? | | false | | +| options | object? | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -803,7 +803,7 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fscreatewri | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | path | string \| Buffer \| URL | | false | | -| options | object? | | false | | +| options | object? | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -820,10 +820,10 @@ Invokes the callback with the <fs.Stats> for the file descriptor. See | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | fd | number | | false | A file descriptor. | -| options | object? \| function? | | false | An options object. | -| callback | function? | | false | The function to call after completion. | +| options | object? \| function? | | true | An options object. | +| callback | function? | | true | The function to call after completion. | -## [`open(path, flags , mode , options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L407) +## [`open(path, flags, mode, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L407) External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fsopenpath-flags-mode-callback Asynchronously open a file calling `callback` upon success or error. @@ -831,10 +831,10 @@ Asynchronously open a file calling `callback` upon success or error. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | path | string \| Buffer \| URL | | false | | -| flags | string? | r | false | | -| mode | string? | 0o666 | false | | -| options | object? \| function? | | false | | -| callback | function(Error?, number?)? | | false | | +| flags | string? | r | true | | +| mode | string? | 0o666 | true | | +| options | object? \| function? | | true | | +| callback | function(Error?, number?)? | | true | | ## [`opendir(path, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L460) @@ -844,9 +844,9 @@ Asynchronously open a directory calling `callback` upon success or error. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | path | string \| Buffer \| URL | | false | | -| options | object? \| function(Error?, Dir?) | | false | | -| options.encoding | string? | utf8 | false | | -| options.withFileTypes | boolean? | false | false | | +| options | object? \| function(Error?, Dir?) | | true | | +| options.encoding | string? | utf8 | true | | +| options.withFileTypes | boolean? | false | true | | | callback | function(Error?, Dir?)? | | false | | ## [`read(fd, buffer, offset, length, position, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L486) @@ -871,9 +871,9 @@ Asynchronously read all entries in a directory. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | path | string \| Buffer \| URL | | false | | -| options | object? \| function(Error?, object) | | false | | -| options.encoding ? utf8 | string? | | false | | -| options.withFileTypes ? false | boolean? | | false | | +| options | object? \| function(Error?, object) | | true | | +| options.encoding ? utf8 | string? | | true | | +| options.withFileTypes ? false | boolean? | | true | | | callback | function(Error?, object) | | false | | ## [`readFile(path, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L571) @@ -883,10 +883,10 @@ Asynchronously read all entries in a directory. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | path | string \| Buffer \| URL \| number | | false | | -| options | object? \| function(Error?, Buffer?) | | false | | -| options.encoding ? utf8 | string? | | false | | -| options.flag ? r | string? | | false | | -| options.signal | AbortSignal? | | false | | +| options | object? \| function(Error?, Buffer?) | | true | | +| options.encoding ? utf8 | string? | | true | | +| options.flag ? r | string? | | true | | +| options.signal | AbortSignal? | | true | | | callback | function(Error?, Buffer?) | | false | | ## [`stat(path, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L690) @@ -897,9 +897,9 @@ Asynchronously read all entries in a directory. | :--- | :--- | :---: | :---: | :--- | | path | string \| Buffer \| URL \| number | | false | filename or file descriptor | | options | object? | | false | | -| options.encoding ? utf8 | string? | | false | | -| options.flag ? r | string? | | false | | -| options.signal | AbortSignal? | | false | | +| options.encoding ? utf8 | string? | | true | | +| options.flag ? r | string? | | true | | +| options.signal | AbortSignal? | | true | | | callback | function(Error?, Stats?) | | false | | ## [`writeFile(path, data, options, callback)`](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L786) @@ -911,10 +911,10 @@ Asynchronously read all entries in a directory. | path | string \| Buffer \| URL \| number | | false | filename or file descriptor | | data | string \| Buffer \| TypedArray \| DataView \| object | | false | | | options | object? | | false | | -| options.encoding ? utf8 | string? | | false | | -| options.mode ? 0o666 | string? | | false | | -| options.flag ? w | string? | | false | | -| options.signal | AbortSignal? | | false | | +| options.encoding ? utf8 | string? | | true | | +| options.mode ? 0o666 | string? | | true | | +| options.flag ? w | string? | | true | | +| options.signal | AbortSignal? | | true | | | callback | function(Error?) | | false | | @@ -950,8 +950,8 @@ Asynchronously check access a file. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | path | string \| Buffer \| URL | | false | | -| mode | string? | | false | | -| options | object? | | false | | +| mode | string? | | true | | +| options | object? | | true | | ## [`chmod(path, mode)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L96) @@ -1005,9 +1005,9 @@ External docs: https://nodejs.org/api/fs.html#fspromisesopendirpath-options | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | path | string \| Buffer \| URL | | false | | -| options | object? | | false | | -| options.encoding | string? | utf8 | false | | -| options.bufferSize | number? | 32 | false | | +| options | object? | | true | | +| options.encoding | string? | utf8 | true | | +| options.bufferSize | number? | 32 | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1022,8 +1022,8 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesr | :--- | :--- | :---: | :---: | :--- | | path | string \| Buffer \| URL | | false | | | options | object? | | false | | -| options.encoding | string? | utf8 | false | | -| options.withFileTypes | boolean? | false | false | | +| options.encoding | string? | utf8 | true | | +| options.withFileTypes | boolean? | false | true | | ## [`readFile(path, options)`](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L293) @@ -1033,10 +1033,10 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesr | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | path | string | | false | | -| options | object? | | false | | -| options.encoding | (string \| null)? | null | false | | -| options.flag | string? | r | false | | -| options.signal | AbortSignal? | | false | | +| options | object? | | true | | +| options.encoding | (string \| null)? | null | true | | +| options.flag | string? | r | true | | +| options.signal | AbortSignal? | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1050,8 +1050,8 @@ External docs: https://nodejs.org/api/fs.html#fspromisesstatpath-options | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | path | string \| Buffer \| URL | | false | | -| options | object? | | false | | -| options.bigint | boolean? | false | false | | +| options | object? | | true | | +| options.bigint | boolean? | false | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1066,11 +1066,11 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesw | :--- | :--- | :---: | :---: | :--- | | path | string \| Buffer \| URL \| FileHandle | | false | filename or FileHandle | | data | string \| Buffer \| Array \| DataView \| TypedArray | | false | | -| options | object? | | false | | -| options.encoding | string \| null | utf8 | false | | -| options.mode | number | 0o666 | false | | -| options.flag | string | w | false | | -| options.signal | AbortSignal? | | false | | +| options | object? | | true | | +| options.encoding | string \| null | utf8 | true | | +| options.mode | number | 0o666 | true | | +| options.flag | string | w | true | | +| options.signal | AbortSignal? | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1107,7 +1107,7 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesw import { send } from 'socket:ipc' ``` -## [`emit(name, value, target , options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1065) +## [`emit(name, value, target, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1065) Emit event to be dispatched on `window` object. @@ -1407,16 +1407,16 @@ Creates a `Path` instance from `input` and optional `cwd`. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | input | PathComponent | | false | | -| cwd | string | | false | | +| cwd | string | | true | | -### [`constructor(pathname, cwd )`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L398) +### [`constructor(pathname, cwd)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L398) `Path` class constructor. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | pathname | string | | false | | -| cwd | string | Path.cwd() | false | | +| cwd | string | Path.cwd() | true | | ### [`isRelative()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L467) @@ -1505,7 +1505,7 @@ Computed high resolution time as a `BigInt`. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | -| time | Array<number>? | | false | | +| time | Array<number>? | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1589,7 +1589,7 @@ Plan the number of assertions. | :--- | :--- | :---: | :---: | :--- | | actual | T | | false | | | expected | T | | false | | -| msg | string | | false | | +| msg | string | | true | | ### [`notDeepEqual(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L163) @@ -1599,7 +1599,7 @@ Plan the number of assertions. | :--- | :--- | :---: | :---: | :--- | | actual | T | | false | | | expected | T | | false | | -| msg | string | | false | | +| msg | string | | true | | ### [`equal(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L178) @@ -1609,7 +1609,7 @@ Plan the number of assertions. | :--- | :--- | :---: | :---: | :--- | | actual | T | | false | | | expected | T | | false | | -| msg | string | | false | | +| msg | string | | true | | ### [`notEqual(actual, expected, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L193) @@ -1619,7 +1619,7 @@ Plan the number of assertions. | :--- | :--- | :---: | :---: | :--- | | actual | unknown | | false | | | expected | unknown | | false | | -| msg | string | | false | | +| msg | string | | true | | ### [`fail(msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L206) @@ -1627,7 +1627,7 @@ Plan the number of assertions. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | -| msg | string | | false | | +| msg | string | | true | | ### [`ok(actual, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L219) @@ -1636,7 +1636,7 @@ Plan the number of assertions. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | actual | unknown | | false | | -| msg | string | | false | | +| msg | string | | true | | ### [`pass(msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L231) @@ -1644,7 +1644,7 @@ Plan the number of assertions. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | -| msg | string | | false | | +| msg | string | | true | | ### [`ifError(err, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L240) @@ -1653,7 +1653,7 @@ Plan the number of assertions. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | err | Error \| null \| undefined | | false | | -| msg | string | | false | | +| msg | string | | true | | ### [`throws(fn, expected, message)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L253) @@ -1662,8 +1662,8 @@ Plan the number of assertions. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | fn | Function | | false | | -| expected | RegExp \| any | | false | | -| message | string | | false | | +| expected | RegExp \| any | | true | | +| message | string | | true | | ### [`sleep(ms, msg)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L302) @@ -1677,7 +1677,7 @@ Sleep for ms with an optional msg | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | ms | number | | false | | -| msg | string | | false | | +| msg | string | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1695,7 +1695,7 @@ Request animation frame with an optional msg. Falls back to a 0ms setTimeout whe | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | -| msg | string | | false | | +| msg | string | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1713,7 +1713,7 @@ Dispatch the `click`` method on an element specified by selector. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | selector | string \| HTMLElement \| Element | | false | A CSS selector string, or an instance of HTMLElement, or Element. | -| msg | string | | false | | +| msg | string | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1731,7 +1731,7 @@ Dispatch the click window.MouseEvent on an element specified by selector. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | selector | string \| HTMLElement \| Element | | false | A CSS selector string, or an instance of HTMLElement, or Element. | -| msg | string | | false | | +| msg | string | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1750,7 +1750,7 @@ Dispatch an event on the target. | :--- | :--- | :---: | :---: | :--- | | event | string \| Event | | false | The event name or Event instance to dispatch. | | target | string \| HTMLElement \| Element | | false | A CSS selector string, or an instance of HTMLElement, or Element to dispatch the event on. | -| msg | string | | false | | +| msg | string | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1768,7 +1768,7 @@ Call the focus method on element specified by selector. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | selector | string \| HTMLElement \| Element | | false | A CSS selector string, or an instance of HTMLElement, or Element. | -| msg | string | | false | | +| msg | string | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1786,7 +1786,7 @@ Call the blur method on element specified by selector. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | selector | string \| HTMLElement \| Element | | false | A CSS selector string, or an instance of HTMLElement, or Element. | -| msg | string | | false | | +| msg | string | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1805,7 +1805,7 @@ Consecutively set the str value of the element specified by selector to simulate | :--- | :--- | :---: | :---: | :--- | | selector | string \| HTMLElement \| Element | | false | A CSS selector string, or an instance of HTMLElement, or Element. | | str | string | | false | The string to type into the :focus element. | -| msg | string | | false | | +| msg | string | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1825,7 +1825,7 @@ appendChild an element el to a parent selector element. | :--- | :--- | :---: | :---: | :--- | | parentSelector | string \| HTMLElement \| Element | | false | A CSS selector string, or an instance of HTMLElement, or Element to appendChild on. | | el | HTMLElement \| Element | | false | A element to append to the parent element. | -| msg | string | | false | | +| msg | string | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1843,7 +1843,7 @@ Remove an element from the DOM. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | selector | string \| HTMLElement \| Element | | false | A CSS selector string, or an instance of HTMLElement, or Element to remove from the DOM. | -| msg | string | | false | | +| msg | string | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1861,7 +1861,7 @@ Test if an element is visible | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | selector | string \| HTMLElement \| Element | | false | A CSS selector string, or an instance of HTMLElement, or Element to test visibility on. | -| msg | string | | false | | +| msg | string | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1879,7 +1879,7 @@ Test if an element is invisible | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | selector | string \| HTMLElement \| Element | | false | A CSS selector string, or an instance of HTMLElement, or Element to test visibility on. | -| msg | string | | false | | +| msg | string | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1897,10 +1897,10 @@ Test if an element is invisible | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | querySelectorOrFn | string \| (() => HTMLElement \| Element \| null \| undefined) | | false | A query string or a function that returns an element. | -| opts | Object | | false | | -| opts.visible | boolean | | false | The element needs to be visible. | -| opts.timeout | number | | false | The maximum amount of time to wait. | -| msg | string | | false | | +| opts | Object | | true | | +| opts.visible | boolean | | true | The element needs to be visible. | +| opts.timeout | number | | true | The maximum amount of time to wait. | +| msg | string | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1929,8 +1929,8 @@ Test if an element is invisible | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | selector | string \| HTMLElement \| Element | | false | A CSS selector string, or an instance of HTMLElement, or Element. | -| opts | WaitForTextOpts \| string \| RegExp | | false | | -| msg | string | | false | | +| opts | WaitForTextOpts \| string \| RegExp | | true | | +| msg | string | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1948,7 +1948,7 @@ Run a querySelector as an assert and also get the results | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | selector | string | | false | A CSS selector string, or an instance of HTMLElement, or Element to select. | -| msg | string | | false | | +| msg | string | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1966,7 +1966,7 @@ Run a querySelectorAll as an assert and also get the results | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | selector | string | | false | A CSS selector string, or an instance of HTMLElement, or Element to select. | -| msg | string | | false | | +| msg | string | | true | | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -1991,7 +1991,7 @@ Retrieves the computed styles for a given element. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | selector | string \| Element | | false | The CSS selector or the Element object for which to get the computed styles. | -| msg | string | | false | An optional message to display when the operation is successful. Default message will be generated based on the type of selector. | +| msg | string | | true | An optional message to display when the operation is successful. Default message will be generated based on the type of selector. | | Return Value | Type | Description | | :--- | :--- | :--- | @@ -2013,7 +2013,7 @@ Retrieves the computed styles for a given element. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | -| report | (lines: string) => void | | false | | +| report | (lines: string) => void | | true | | ### [`nextId()`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L953) @@ -2041,7 +2041,7 @@ Retrieves the computed styles for a given element. | :--- | :--- | :--- | | Not specified | Promise<void> | | -### [`onFinish() )`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1032) +### [`onFinish())`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1032) @@ -2056,7 +2056,7 @@ Retrieves the computed styles for a given element. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | name | string | | false | | -| fn | TestFn | | false | | +| fn | TestFn | | true | | ## [`skip(_name, _fn)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1070) @@ -2065,7 +2065,7 @@ Retrieves the computed styles for a given element. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | | _name | string | | false | | -| _fn | TestFn | | false | | +| _fn | TestFn | | true | | ## [`setStrict(strict)`](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L1076) diff --git a/api/application.js b/api/application.js index 718e8585d2..3c02965388 100644 --- a/api/application.js +++ b/api/application.js @@ -147,10 +147,10 @@ export async function getCurrentWindow () { /** * Quits the backend process and then quits the render process, the exit code used is the final exit code to the OS. - * @param {number} [code] - an exit code + * @param {number} [code = 0] - an exit code * @return {Promise<ipc.Result>} */ -export async function exit (code) { +export async function exit (code = 0) { const { data, err } = await ipc.send('application.exit', code) if (err) { throw err diff --git a/api/index.d.ts b/api/index.d.ts index 66dc46e618..d1afb94554 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -3409,7 +3409,7 @@ declare module "socket:application" { export function getCurrentWindow(): Promise<ApplicationWindow>; /** * Quits the backend process and then quits the render process, the exit code used is the final exit code to the OS. - * @param {number} [code] - an exit code + * @param {number} [code = 0] - an exit code * @return {Promise<ipc.Result>} */ export function exit(code?: number): Promise<ipc.Result>; From 3510f078f4f9a665c749a2191602beb035999f4d Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Wed, 4 Oct 2023 18:48:53 -0400 Subject: [PATCH 067/256] fix(cli): run user build script after package is scaffolded In my case, this is useful for manipulating the Info.plist file --- src/cli/cli.cc | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index ea50a7c5f5..0d153ab19a 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -2640,21 +2640,6 @@ int main (const int argc, const char* argv[]) { additionalBuildArgs += " --test=true"; } - handleBuildPhaseForUserScript( - settings, - targetPlatform, - pathResourcesRelativeToUserBuild, - oldCwd, - additionalBuildArgs, - true - ); - - auto copyMapFiles = handleBuildPhaseForCopyMappedFiles( - settings, - targetPlatform, - pathResourcesRelativeToUserBuild - ); - String flags; String files; @@ -4175,6 +4160,21 @@ int main (const int argc, const char* argv[]) { // TODO Copy the files into place } + handleBuildPhaseForUserScript( + settings, + targetPlatform, + pathResourcesRelativeToUserBuild, + oldCwd, + additionalBuildArgs, + true + ); + + auto copyMapFiles = handleBuildPhaseForCopyMappedFiles( + settings, + targetPlatform, + pathResourcesRelativeToUserBuild + ); + log("package prepared"); auto SOCKET_HOME_API = getEnv("SOCKET_HOME_API"); From 235904e03db91cec2e479880170e6c3c18fa38df Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Fri, 6 Oct 2023 17:31:32 -0400 Subject: [PATCH 068/256] fix(cli): warn when extension sources are not found --- src/cli/cli.cc | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 0d153ab19a..3fe09a827a 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -4566,9 +4566,19 @@ int main (const int argc, const char* argv[]) { auto source = settings["build_extensions_" + extension + "_source"]; - if (source.size() == 0 && fs::is_directory(settings["build_extensions_" + extension])) { + if (source.size() == 0) { source = settings["build_extensions_" + extension]; - settings["build_extensions_" + extension] = ""; + if (source.size() > 0) { + if (fs::is_directory(source)) { + settings["build_extensions_" + extension] = ""; + } else { + log( + "\033[33mWARN\033[0m " + key + " is not a directory, ignoring: " + + fs::absolute(source).string() + ); + source = ""; + } + } } if (source.size() > 0) { @@ -4607,6 +4617,13 @@ int main (const int argc, const char* argv[]) { } } } + + if (settings["build_extensions_" + extension].size() == 0) { + log( + "\033[33mWARN\033[0m " + key + " has no sources, ignoring: " + + fs::canonical(configFile).string() + ); + } } } From eb83a3a25abf2cb3472b1f3b4fac188e445edcff Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sat, 16 Sep 2023 18:41:49 -0400 Subject: [PATCH 069/256] feat(cli): respect NO_ANDROID=1 in `ssc build` --- bin/functions.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bin/functions.sh b/bin/functions.sh index 092c1e15ae..ea5cb18ce5 100755 --- a/bin/functions.sh +++ b/bin/functions.sh @@ -558,6 +558,10 @@ function first_time_experience_setup() { export BUILD_ANDROID="1" local target="$1" + if [[ -n "$NO_ANDROID" ]]; then + unset BUILD_ANDROID + fi + if [ -z "$target" ] || [[ "$target" == "linux" ]]; then if [[ "$(host_os)" == "Linux" ]]; then local package_manager="$(determine_package_manager)" From 47bda25e4264c9776bde9d9c61d322537630e14c Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Fri, 6 Oct 2023 16:29:11 -0400 Subject: [PATCH 070/256] fix(cli): --quiet typo --- src/cli/cli.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 3fe09a827a..b36c06b247 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -2471,7 +2471,7 @@ int main (const int argc, const char* argv[]) { argvForward += " --headless"; } - if (optionsWithoutValue.find("--quite") != optionsWithoutValue.end() || equal(rc["build_quiet"], "true")) { + if (optionsWithoutValue.find("--quiet") != optionsWithoutValue.end() || equal(rc["build_quiet"], "true")) { flagQuietMode = true; } From c6416ab043f1aa96e27201e0accf3b1e6679e3b2 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sat, 16 Sep 2023 17:41:51 -0400 Subject: [PATCH 071/256] fix: encode uncaught exception before writing to ipc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fix encodes any `\n` characters for the ipc://stderr message. The `String(โ€ฆ)` call is not strictly necessary, but keeps tsc satisfied. --- npm/packages/@socketsupply/socket-node/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm/packages/@socketsupply/socket-node/index.js b/npm/packages/@socketsupply/socket-node/index.js index 88e248cac3..463ce4cda4 100644 --- a/npm/packages/@socketsupply/socket-node/index.js +++ b/npm/packages/@socketsupply/socket-node/index.js @@ -26,7 +26,7 @@ class API { this.#write(`ipc://process.exit?value=${exitCode}`) }) process.on('uncaughtException', (err) => { - this.#write(`ipc://stderr?value=${err}`) + this.#write(`ipc://stderr?value=${encodeURIComponent(String(err))}`) }) // redirect console From 694412a2b492b03421c77804fa6457f7feccd4bd Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Wed, 4 Oct 2023 12:03:26 -0400 Subject: [PATCH 072/256] fix: remove superfluous decodeURIComponent calls The IPC::Message constructor decodes the `value` and `seq` properties by default. If you pass true, the arguments will be decoded too. --- src/desktop/main.cc | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/src/desktop/main.cc b/src/desktop/main.cc index 51f1a78780..2a98e031af 100644 --- a/src/desktop/main.cc +++ b/src/desktop/main.cc @@ -246,7 +246,7 @@ MAIN { exitCode = stoi(message.get("value")); exit(exitCode); } else { - stdWrite(decodeURIComponent(message.get("value")), false); + stdWrite(message.get("value"), false); } }, [](SSC::String const &out) { stdWrite(out, true); }, @@ -299,7 +299,7 @@ MAIN { // just stdout and we can write the data to the pipe. // app.dispatch([&, out] { - IPC::Message message(out); + IPC::Message message(out, true); auto value = message.get("value"); auto seq = message.get("seq"); @@ -315,22 +315,20 @@ MAIN { } if (message.name == "send") { + SSC::String script = getEmitToRenderProcessJavaScript( + message.get("event"), + value + ); if (message.index >= 0) { auto window = windowManager.getWindow(message.index); if (window) { - window->eval(getEmitToRenderProcessJavaScript( - decodeURIComponent(message.get("event")), - value - )); + window->eval(script); } } else { for (auto w : windowManager.windows) { if (w != nullptr) { auto window = windowManager.getWindow(w->opts.index); - window->eval(getEmitToRenderProcessJavaScript( - decodeURIComponent(message.get("event")), - value - )); + window->eval(script); } } } @@ -407,7 +405,7 @@ MAIN { // auto onMessage = [&](auto out) { // debug("onMessage %s", out.c_str()); - IPC::Message message(out); + IPC::Message message(out, true); auto window = windowManager.getWindow(message.index); auto value = message.get("value"); @@ -470,7 +468,7 @@ MAIN { if (message.name == "window.send") { const auto event = message.get("event"); - const auto value = decodeURIComponent(message.get("value")); + const auto value = message.get("value"); const auto targetWindowIndex = message.get("targetWindowIndex").size() >= 0 ? std::stoi(message.get("targetWindowIndex")) : -1; const auto targetWindow = windowManager.getWindow(targetWindowIndex); const auto currentWindow = windowManager.getWindow(message.index); @@ -484,7 +482,7 @@ MAIN { if (message.name == "application.exit") { try { - exitCode = std::stoi(decodeURIComponent(value)); + exitCode = std::stoi(value); } catch (...) { } @@ -548,7 +546,7 @@ MAIN { return; } - SSC::String error = getNavigationError(cwd, decodeURIComponent(message.get("url"))); + SSC::String error = getNavigationError(cwd, message.get("url")); if (error.size() > 0) { const JSON::Object json = SSC::JSON::Object::Entries { {"err", JSON::Object::Entries { @@ -733,7 +731,7 @@ MAIN { const auto targetWindowIndex = message.get("targetWindowIndex").size() > 0 ? std::stoi(message.get("targetWindowIndex")) : currentIndex; const auto targetWindow = windowManager.getWindow(targetWindowIndex); const auto url = message.get("url"); - const auto error = getNavigationError(cwd, decodeURIComponent(url)); + const auto error = getNavigationError(cwd, url); if (error.size() > 0) { JSON::Object json = JSON::Object::Entries { @@ -852,7 +850,7 @@ MAIN { if (message.name == "application.setSystemMenu") { const auto seq = message.get("seq"); - window->setSystemMenu(seq, decodeURIComponent(value)); + window->setSystemMenu(seq, value); return; } @@ -888,9 +886,9 @@ MAIN { bool bDirs = message.get("allowDirs").compare("true") == 0; bool bFiles = message.get("allowFiles").compare("true") == 0; bool bMulti = message.get("allowMultiple").compare("true") == 0; - SSC::String defaultName = decodeURIComponent(message.get("defaultName")); - SSC::String defaultPath = decodeURIComponent(message.get("defaultPath")); - SSC::String title = decodeURIComponent(message.get("title")); + SSC::String defaultName = message.get("defaultName"); + SSC::String defaultPath = message.get("defaultPath"); + SSC::String title = message.get("title"); window->openDialog(message.get("seq"), bSave, bDirs, bFiles, bMulti, defaultPath, title, defaultName); return; @@ -898,7 +896,7 @@ MAIN { if (message.name == "window.setContextMenu") { auto seq = message.get("seq"); - window->setContextMenu(seq, decodeURIComponent(value)); + window->setContextMenu(seq, value); window->resolvePromise( message.seq, OK_STATE, From 22172a07bba17fb4403457bc75624114c59ebd74 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sat, 16 Sep 2023 17:46:02 -0400 Subject: [PATCH 073/256] feat: write backend logs to stdout/stderr --- src/desktop/main.cc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/desktop/main.cc b/src/desktop/main.cc index 2a98e031af..fa5dcce1ec 100644 --- a/src/desktop/main.cc +++ b/src/desktop/main.cc @@ -279,6 +279,8 @@ MAIN { } auto onStdErr = [&](auto err) { + std::cerr << "\033[31m" + err + "\033[0m"; + for (auto w : windowManager.windows) { if (w != nullptr) { auto window = windowManager.getWindow(w->opts.index); @@ -376,6 +378,16 @@ MAIN { } return; } + + if (message.name == "stdout") { + std::cout << value; + return; + } + + if (message.name == "stderr") { + std::cerr << "\033[31m" + value + "\033[0m"; + return; + } }); }; From 60b773c1c514f5fcdc21f58a019570fa7bfae7fa Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sun, 24 Sep 2023 21:00:52 -0400 Subject: [PATCH 074/256] feat(mac): forward backend logs to Console.app --- src/desktop/main.cc | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/desktop/main.cc b/src/desktop/main.cc index fa5dcce1ec..0c99c43584 100644 --- a/src/desktop/main.cc +++ b/src/desktop/main.cc @@ -22,6 +22,10 @@ int main (int argc, char** argv) #endif +#if defined(__APPLE__) +#include <os/log.h> +#endif + #define InvalidWindowIndexError(index) \ SSC::String("Invalid index given for window: ") + std::to_string(index) @@ -278,7 +282,22 @@ MAIN { return exitCode; } +#if defined(__APPLE__) + static auto userConfig = SSC::getUserConfig(); + static auto bundleIdentifier = userConfig["meta_bundle_identifier"]; + static auto SSC_OS_LOG_BUNDLE = os_log_create(bundleIdentifier.c_str(), + #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + "socket.runtime.mobile" + #else + "socket.runtime.desktop" + #endif + ); +#endif + auto onStdErr = [&](auto err) { + #if defined(__APPLE__) + os_log_with_type(SSC_OS_LOG_BUNDLE, OS_LOG_TYPE_ERROR, "%{public}s", err.c_str()); + #endif std::cerr << "\033[31m" + err + "\033[0m"; for (auto w : windowManager.windows) { @@ -380,11 +399,21 @@ MAIN { } if (message.name == "stdout") { + #if defined(__APPLE__) + dispatch_async(dispatch_get_main_queue(), ^{ + os_log_with_type(SSC_OS_LOG_BUNDLE, OS_LOG_TYPE_DEFAULT, "%{public}s", value.c_str()); + }); + #endif std::cout << value; return; } if (message.name == "stderr") { + #if defined(__APPLE__) + dispatch_async(dispatch_get_main_queue(), ^{ + os_log_with_type(SSC_OS_LOG_BUNDLE, OS_LOG_TYPE_ERROR, "%{public}s", value.c_str()); + }); + #endif std::cerr << "\033[31m" + value + "\033[0m"; return; } From 9e050d684850742fdae63f7709e6812c27cfdf65 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Wed, 4 Oct 2023 11:59:51 -0400 Subject: [PATCH 075/256] fix(backend): move stdout/stderr handling off of main thread This also moves IPC message parsing off the main thread. --- src/desktop/main.cc | 69 +++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/src/desktop/main.cc b/src/desktop/main.cc index 0c99c43584..facf9a305d 100644 --- a/src/desktop/main.cc +++ b/src/desktop/main.cc @@ -313,28 +313,49 @@ MAIN { // Launch the backend process and connect callbacks to the stdio and stderr pipes. // auto onStdOut = [&](SSC::String const &out) { + IPC::Message message(out); + + if (message.index > 0 && message.name.size() == 0) { + // @TODO: print warning + return; + } + + if (message.index > SSC_MAX_WINDOWS) { + // @TODO: print warning + return; + } + + auto value = message.get("value"); + + if (message.name == "stdout") { +#if defined(__APPLE__) + dispatch_async(dispatch_get_main_queue(), ^{ + os_log_with_type(SSC_OS_LOG_BUNDLE, OS_LOG_TYPE_DEFAULT, "%{public}s", value.c_str()); + }); +#endif + std::cout << value; + return; + } + + if (message.name == "stderr") { +#if defined(__APPLE__) + dispatch_async(dispatch_get_main_queue(), ^{ + os_log_with_type(SSC_OS_LOG_BUNDLE, OS_LOG_TYPE_ERROR, "%{public}s", value.c_str()); + }); +#endif + std::cerr << "\033[31m" + value + "\033[0m"; + return; + } + // // ## Dispatch // Messages from the backend process may be sent to the render process. If they // are parsable commands, try to do something with them, otherwise they are // just stdout and we can write the data to the pipe. // - app.dispatch([&, out] { - IPC::Message message(out, true); - - auto value = message.get("value"); + app.dispatch([&, message, value] { auto seq = message.get("seq"); - if (message.index > 0 && message.name.size() == 0) { - // @TODO: print warning - return; - } - - if (message.index > SSC_MAX_WINDOWS) { - // @TODO: print warning - return; - } - if (message.name == "send") { SSC::String script = getEmitToRenderProcessJavaScript( message.get("event"), @@ -397,26 +418,6 @@ MAIN { } return; } - - if (message.name == "stdout") { - #if defined(__APPLE__) - dispatch_async(dispatch_get_main_queue(), ^{ - os_log_with_type(SSC_OS_LOG_BUNDLE, OS_LOG_TYPE_DEFAULT, "%{public}s", value.c_str()); - }); - #endif - std::cout << value; - return; - } - - if (message.name == "stderr") { - #if defined(__APPLE__) - dispatch_async(dispatch_get_main_queue(), ^{ - os_log_with_type(SSC_OS_LOG_BUNDLE, OS_LOG_TYPE_ERROR, "%{public}s", value.c_str()); - }); - #endif - std::cerr << "\033[31m" + value + "\033[0m"; - return; - } }); }; From 8d1cec7e7b7116ac8d0b49afcedbae3f136f4194 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Fri, 6 Oct 2023 18:53:24 -0400 Subject: [PATCH 076/256] chore: use pnpm workspaces --- .npmrc | 2 ++ pnpm-workspace.yaml | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 .npmrc create mode 100644 pnpm-workspace.yaml diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..4431f30b2f --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +no-lockfile=true +ignore-workspace-root-check=true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000000..94b4107017 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - npm/packages/@socketsupply/socket-node From 43074636f522f06384dcb34a60b3a7e60535ff66 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Fri, 6 Oct 2023 19:46:26 -0400 Subject: [PATCH 077/256] fix(cli): log `configure.script` output when exitCode is non-zero This makes it easier to debug an extension's `configure.script` when it fails. --- src/cli/cli.cc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index b36c06b247..cd135c69f5 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -4657,7 +4657,13 @@ int main (const int argc, const char* argv[]) { } if (configure.size() > 0) { - auto output = replace(exec(configure + argvForward).output, "\n", " "); + SSC::ExecOutput result = exec(configure + argvForward); + if (result.exitCode != 0) { + log("ERROR: failed to configure extension: " + extension); + log(result.output); + exit(result.exitCode); + } + auto output = replace(result.output, "\n", " "); if (output.size() > 0) { for (const auto& source : parseStringList(output, ' ')) { sources.push_back(source); From 056dadeef07903907cd8a411f2b1e4aad3ee2adb Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Thu, 5 Oct 2023 19:28:21 -0400 Subject: [PATCH 078/256] chore: add exports field to @socketsupply/socket package In my experience, this can help tsc language server find the typings. --- npm/packages/@socketsupply/socket/package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/npm/packages/@socketsupply/socket/package.json b/npm/packages/@socketsupply/socket/package.json index 97ea6cdfc9..7feda7d1a5 100644 --- a/npm/packages/@socketsupply/socket/package.json +++ b/npm/packages/@socketsupply/socket/package.json @@ -5,6 +5,10 @@ "type": "module", "main": "index.js", "types": "index.d.ts", + "exports": { + "types": "./index.d.ts", + "default": "./index.js" + }, "bin": { "ssc": "bin/ssc.js" }, From f74451827758fa182dbeabca4889696cab87efd1 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sat, 16 Sep 2023 17:41:22 -0400 Subject: [PATCH 079/256] fix: use --force with npm link When running `bin/publish-npm-modules.sh --link` repeatedly, the `npm link` can fail if you don't use `--force`. --- bin/publish-npm-modules.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/publish-npm-modules.sh b/bin/publish-npm-modules.sh index 1862b702dd..9f181bc462 100755 --- a/bin/publish-npm-modules.sh +++ b/bin/publish-npm-modules.sh @@ -198,7 +198,7 @@ if (( !only_top_level )); then _publish if (( do_global_link )); then - npm link --no-fund --no-audit --offline + npm link --no-fund --no-audit --offline --force fi done fi @@ -212,9 +212,9 @@ if (( !only_platforms || only_top_level )); then if (( do_global_link )); then for arch in "${archs[@]}"; do declare package="@socketsupply/socket-$platform-${arch/x86_64/x64}" - npm link --no-fund --no-audit --offline "$package" + npm link --no-fund --no-audit --offline --force "$package" done - npm link --no-fund --no-audit --offline + npm link --no-fund --no-audit --offline --force fi fi From 9c0978d1ff5791e9789072fb86ba891bd1bb0005 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sun, 17 Sep 2023 17:30:21 -0400 Subject: [PATCH 080/256] fix: send all process.stdout/process.stderr writes through ipc In some cases, user-created logs may be written directly to `process.stdout` and `process.stderr` so this PR intercepts those too. Another PR incoming that will print the Node.js logs in the main process and send them to Console.app with os_log on macOS/iOS. --- .../@socketsupply/socket-node/index.js | 57 +++++++++++++------ 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/npm/packages/@socketsupply/socket-node/index.js b/npm/packages/@socketsupply/socket-node/index.js index 463ce4cda4..0c454c17db 100644 --- a/npm/packages/@socketsupply/socket-node/index.js +++ b/npm/packages/@socketsupply/socket-node/index.js @@ -16,6 +16,8 @@ class API { #buf = '' #emitter = new EventEmitter() + #writeStdout + #writeStderr constructor () { process.stdin.resume() @@ -29,18 +31,39 @@ class API { this.#write(`ipc://stderr?value=${encodeURIComponent(String(err))}`) }) - // redirect console - console.log = (...args) => { - const s = args.map(v => format(v)).join(' ') - const enc = encodeURIComponent(s) - this.#write(`ipc://stdout?value=${enc}`) - } - console.error = (...args) => { - const s = args.map(v => format(v)).join(' ') - const enc = encodeURIComponent(s) - this.#write(`ipc://stderr?value=${enc}`) + function overrideStreamWrite (stream, write) { + const protoWrite = Object.getPrototypeOf(stream).write + stream.write = write + return protoWrite.bind(stream) } + this.#writeStdout = overrideStreamWrite( + process.stdout, + (data, encoding, callback) => { + if (typeof data !== 'string') { + this.#writeStdout(data, encoding, callback) + return + } + if (!data.startsWith('ipc://')) { + data = 'ipc://stdout?value=' + encodeURIComponent(data) + } + this.#write(data) + } + ) + this.#writeStderr = overrideStreamWrite( + process.stderr, + (data, encoding, callback) => { + if (typeof data !== 'string') { + this.#writeStderr(data, encoding, callback) + return + } + if (!data.startsWith('ipc://')) { + data = 'ipc://stderr?value=' + encodeURIComponent(data) + } + this.#write(data) + } + ) + for (const arg of process.argv) { if (arg.startsWith(API.#sscVersionPrefix)) { const [major, minor, patch] = arg.match(API.#sscVersionPattern)?.[1].split('.').map(Number) ?? [0, 0, 0] @@ -85,10 +108,10 @@ class API { if (data.length > MAX_MESSAGE_KB) { const len = Math.ceil(data.length / 1024) - process.stderr.write( + this.#writeStderr( 'WARNING: Receiving large message from webview: ' + len + 'kb\n' ) - process.stderr.write('RAW MESSAGE: ' + data.slice(0, 512) + '...\n') + this.#writeStderr('RAW MESSAGE: ' + data.slice(0, 512) + '...\n') } try { @@ -144,13 +167,13 @@ class API { if (s.length > MAX_MESSAGE_KB) { const len = Math.ceil(s.length / 1024) - process.stderr.write('WARNING: Sending large message to webview: ' + len + 'kb\n') - process.stderr.write('RAW MESSAGE: ' + s.slice(0, 512) + '...\n') + this.#writeStderr( + 'WARNING: Sending large message to webview: ' + len + 'kb\n' + ) + this.#writeStderr('RAW MESSAGE: ' + s.slice(0, 512) + '...\n') } - return new Promise(resolve => - process.stdout.write(s + '\n', resolve) - ) + return new Promise((resolve) => this.#writeStdout(s + '\n', resolve)) } // From dba3f346e6eff12b9e4c92a8c7ee380de81a191e Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sat, 7 Oct 2023 15:19:49 -0400 Subject: [PATCH 081/256] fix(extension): long->int precision loss --- include/socket/extension.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/socket/extension.h b/include/socket/extension.h index 8465e61d4f..fbbd5a7ac3 100644 --- a/include/socket/extension.h +++ b/include/socket/extension.h @@ -64,7 +64,7 @@ }; \ \ SOCKET_RUNTIME_EXTENSION_EXPORT \ - unsigned int __sapi_extension_abi () { \ + unsigned long __sapi_extension_abi () { \ return __sapi_extension__.abi; \ } \ \ From 2b746bb2bbdf9fd0d7ba88ba314ec6bd517aeeef Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sat, 7 Oct 2023 16:51:20 -0400 Subject: [PATCH 082/256] feat(extension): add `sapi_ipc_reply_with_error` function --- include/socket/extension.h | 9 +++++++++ src/extension/ipc.cc | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/include/socket/extension.h b/include/socket/extension.h index fbbd5a7ac3..3d3659cc3a 100644 --- a/include/socket/extension.h +++ b/include/socket/extension.h @@ -1104,6 +1104,15 @@ extern "C" { SOCKET_RUNTIME_EXTENSION_EXPORT bool sapi_ipc_reply (const sapi_ipc_result_t* result); + /** + * Convenience method for replying with an error message. + * @param result - An IPC request result + * @param error - An error message to include in the result + * @return `true` if successful, otherwise `false` + */ + SOCKET_RUNTIME_EXTENSION_EXPORT + bool sapi_ipc_reply_with_error (sapi_ipc_result_t* result, const char* error); + /** * Send JSON to the bridge to propagate to the WebView. * @param context - An extension context diff --git a/src/extension/ipc.cc b/src/extension/ipc.cc index b339c0d21d..0ebc9a2cc9 100644 --- a/src/extension/ipc.cc +++ b/src/extension/ipc.cc @@ -112,6 +112,15 @@ bool sapi_ipc_router_unlisten ( return ctx->router->unlisten(name, token); } +bool sapi_ipc_reply_with_error (sapi_ipc_result_t* result, const char* error) { + sapi_context* context = sapi_ipc_result_get_context(result); + sapi_json_string* errorJson = sapi_json_string_create(context, error); + sapi_json_object* errorObject = sapi_json_object_create(context); + sapi_json_object_set(errorObject, "message", errorJson); + sapi_ipc_result_set_json_error(result, ((sapi_json_any*)errorObject)); + return sapi_ipc_reply(result); +} + bool sapi_ipc_reply (const sapi_ipc_result_t* result) { if (result == nullptr) return false; if (result->context == nullptr) return false; From 0bae82ca401e98f9daedf258073aa8ade6a69c36 Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Mon, 9 Oct 2023 02:08:00 +0200 Subject: [PATCH 083/256] chore(socket-node): remove unused import --- npm/packages/@socketsupply/socket-node/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/npm/packages/@socketsupply/socket-node/index.js b/npm/packages/@socketsupply/socket-node/index.js index 0c454c17db..c604710d4f 100644 --- a/npm/packages/@socketsupply/socket-node/index.js +++ b/npm/packages/@socketsupply/socket-node/index.js @@ -1,5 +1,4 @@ // @ts-check -import { format } from 'node:util' import { EventEmitter } from 'node:events' const MAX_MESSAGE_KB = 512 * 1024 From 1de4d610b8dd79dd927922cb0eb4b03de6014cf6 Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Mon, 9 Oct 2023 02:10:27 +0200 Subject: [PATCH 084/256] chore(socket-node): update docs --- npm/packages/@socketsupply/socket-node/API.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/npm/packages/@socketsupply/socket-node/API.md b/npm/packages/@socketsupply/socket-node/API.md index c34e3f5db2..9535c6f146 100644 --- a/npm/packages/@socketsupply/socket-node/API.md +++ b/npm/packages/@socketsupply/socket-node/API.md @@ -1,4 +1,4 @@ -### [`send(options)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L168) +### [`send(options)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L190) Send event to webview via IPC @@ -13,7 +13,7 @@ Send event to webview via IPC | :--- | :--- | :--- | | Not specified | Promise<Error \| undefined> | | -### [`heartbeat()`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L198) +### [`heartbeat()`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L220) Send the heartbeat event to the webview. @@ -21,7 +21,7 @@ Send the heartbeat event to the webview. | :--- | :--- | :--- | | Not specified | Promise<Error \| undefined> | | -### [`addListener(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L209) +### [`addListener(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L231) Adds a listener to the window. @@ -30,7 +30,7 @@ Adds a listener to the window. | event | string | | false | the event to listen to | | cb | function(*): void | | false | the callback to call | -### [`on(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L220) +### [`on(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L242) Adds a listener to the window. An alias for `addListener`. @@ -39,7 +39,7 @@ Adds a listener to the window. An alias for `addListener`. | event | string | | false | the event to listen to | | cb | function(*): void | | false | the callback to call | -### [`once(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L230) +### [`once(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L252) Adds a listener to the window. The listener is removed after the first call. @@ -48,7 +48,7 @@ Adds a listener to the window. The listener is removed after the first call. | event | string | | false | the event to listen to | | cb | function(*): void | | false | the callback to call | -### [`removeListener(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L240) +### [`removeListener(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L262) Removes a listener from the window. @@ -57,7 +57,7 @@ Removes a listener from the window. | event | string | | false | the event to remove the listener from | | cb | function(*): void | | false | the callback to remove | -### [`removeAllListeners(event)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L249) +### [`removeAllListeners(event)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L271) Removes all listeners from the window. @@ -65,7 +65,7 @@ Removes all listeners from the window. | :--- | :--- | :---: | :---: | :--- | | event | string | | false | the event to remove the listeners from | -### [`off(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L260) +### [`off(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L282) Removes a listener from the window. An alias for `removeListener`. From 9cdd1f0ef24614a698dce49994f69435e63875f7 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sun, 8 Oct 2023 11:59:05 -0400 Subject: [PATCH 085/256] fix(cli): skip build.script when looking for extension sources --- src/cli/cli.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index cd135c69f5..b52eb5e587 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -4546,6 +4546,7 @@ int main (const int argc, const char* argv[]) { if (key.find("linker_flags") != String::npos) continue; if (key.find("linker_debug_flags") != String::npos) continue; if (key.find("configure_script") != String::npos) continue; + if (key.find("build_script") != String::npos) continue; if (key.starts_with("build_extensions_ios_")) continue; if (key.starts_with("build_extensions_android_")) continue; if (key.ends_with("_path")) continue; From 7630c6c75e3689c2cca260876e91a59bb60834a4 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sat, 7 Oct 2023 11:06:49 -0400 Subject: [PATCH 086/256] fix: have `ipc.request` treat `params` argument as `params.value` if not a plain object - Introducing the `IPCSearchParams` class, which abstracts all the index/seq/value handling for send/sendSync/request/write to share --- api/ipc.js | 106 ++++++++++++++++++++++------------------------------- 1 file changed, 44 insertions(+), 62 deletions(-) diff --git a/api/ipc.js b/api/ipc.js index 6ef3472a42..9c3c2f0219 100644 --- a/api/ipc.js +++ b/api/ipc.js @@ -1001,16 +1001,39 @@ export async function ready () { }) } +const { toString } = Object.prototype + +class IPCSearchParams extends URLSearchParams { + constructor (params, nonce) { + const index = params?.index ?? globalThis.__args?.index ?? 0 + const seq = 'R' + nextSeq++ + + super({ + value: params !== undefined && toString.call(params) !== '[object Object]' + ? { value: params } + : undefined, + ...params, + index, + seq, + nonce + }) + } + + toString () { + return super.toString().replace(/\+/g, '%20') + } +} + /** * Sends a synchronous IPC command over XHR returning a `Result` * upon success or error. * @param {string} command - * @param {object|string?} [params] + * @param {any?} [value] * @param {object?} [options] * @return {Result} * @ignore */ -export function sendSync (command, params = {}, options = {}) { +export function sendSync (command, value, options = {}) { if (!globalThis.XMLHttpRequest) { const err = new Error('XMLHttpRequest is not supported in environment') return Result.from(err) @@ -1021,23 +1044,15 @@ export function sendSync (command, params = {}, options = {}) { } const request = new globalThis.XMLHttpRequest() - const index = globalThis.__args?.index ?? 0 - const seq = nextSeq++ - const uri = `ipc://${command}` - - params = new URLSearchParams(params) - params.set('index', index) - params.set('seq', 'R' + seq) - params.set('nonce', Date.now()) - - const query = `?${params}` + const params = new IPCSearchParams(value, Date.now()) + const uri = `ipc://${command}?${params}` if (debug.enabled) { - debug.log('ipc.sendSync: %s', uri + query) + debug.log('ipc.sendSync: %s', uri) } request.responseType = options?.responseType ?? '' - request.open('GET', uri + query, false) + request.open('GET', uri, false) request.send() const response = getRequestResponse(request, options) @@ -1132,36 +1147,17 @@ export async function send (command, value, options) { debug.log('ipc.send:', command, value) } - const seq = 'R' + nextSeq++ - const index = value?.index ?? globalThis.__args?.index ?? 0 - let serialized = '' - - try { - if (value !== undefined && ({}).toString.call(value) !== '[object Object]') { - value = { value } - } - - const params = { - ...value, - index, - seq - } - - serialized = new URLSearchParams(params).toString() - serialized = serialized.replace(/\+/g, '%20') - } catch (err) { - console.error(`${err.message} (${serialized})`) - return Promise.reject(err.message) - } + const params = new IPCSearchParams(value) + const uri = `ipc://${command}?${params}` if (options?.bytes) { - postMessage(`ipc://${command}?${serialized}`, options?.bytes) + postMessage(uri, options.bytes) } else { - postMessage(`ipc://${command}?${serialized}`) + postMessage(uri) } return await new Promise((resolve) => { - const event = `resolve-${index}-${seq}` + const event = `resolve-${params.get('index')}-${params.get('seq')}` globalThis.addEventListener(event, onresolve, { once: true }) function onresolve (event) { const result = Result.from(event.detail, null, command) @@ -1181,12 +1177,12 @@ export async function send (command, value, options) { /** * Sends an async IPC command request with parameters and buffered bytes. * @param {string} command - * @param {object=} params + * @param {any=} value * @param {(Buffer|Uint8Array|ArrayBuffer|string|Array)=} buffer * @param {object=} options * @ignore */ -export async function write (command, params, buffer, options) { +export async function write (command, value, buffer, options) { if (!globalThis.XMLHttpRequest) { const err = new Error('XMLHttpRequest is not supported in environment') return Result.from(err) @@ -1196,9 +1192,8 @@ export async function write (command, params, buffer, options) { const signal = options?.signal const request = new globalThis.XMLHttpRequest() - const index = globalThis?.__args?.index ?? 0 - const seq = nextSeq++ - const uri = `ipc://${command}` + const params = new IPCSearchParams(value, Date.now()) + const uri = `ipc://${command}?${params}` let resolved = false let aborted = false @@ -1217,19 +1212,12 @@ export async function write (command, params, buffer, options) { }) } - params = new URLSearchParams(params) - params.set('index', index) - params.set('seq', 'R' + seq) - params.set('nonce', Date.now()) - - const query = `?${params}` - request.responseType = options?.responseType ?? '' - request.open('POST', uri + query, true) + request.open('POST', uri, true) await request.send(buffer || null) if (debug.enabled) { - debug.log('ipc.write:', uri + query, buffer || null) + debug.log('ipc.write:', uri, buffer || null) } return await new Promise((resolve) => { @@ -1283,11 +1271,11 @@ export async function write (command, params, buffer, options) { * Sends an async IPC command request with parameters requesting a response * with buffered bytes. * @param {string} command - * @param {object=} params + * @param {any=} value * @param {object=} options * @ignore */ -export async function request (command, params, options) { +export async function request (command, value, options) { if (!globalThis.XMLHttpRequest) { const err = new Error('XMLHttpRequest is not supported in environment') return Result.from(err) @@ -1299,10 +1287,9 @@ export async function request (command, params, options) { await ready() - const request = new globalThis.XMLHttpRequest() const signal = options?.signal - const index = globalThis?.__args?.index ?? 0 - const seq = nextSeq++ + const request = new globalThis.XMLHttpRequest() + const params = new IPCSearchParams(value, Date.now()) const uri = `ipc://${command}` let resolved = false @@ -1322,11 +1309,6 @@ export async function request (command, params, options) { }) } - params = new URLSearchParams(params) - params.set('index', index) - params.set('seq', 'R' + seq) - params.set('nonce', Date.now()) - const query = `?${params}` request.responseType = options?.responseType ?? '' From ee37fc17eb7ccd14f393ee778e0a623bb13e9e3e Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sat, 7 Oct 2023 11:44:20 -0400 Subject: [PATCH 087/256] chore(fix): accidental object param --- api/ipc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/ipc.js b/api/ipc.js index 9c3c2f0219..50650ca838 100644 --- a/api/ipc.js +++ b/api/ipc.js @@ -1010,7 +1010,7 @@ class IPCSearchParams extends URLSearchParams { super({ value: params !== undefined && toString.call(params) !== '[object Object]' - ? { value: params } + ? params : undefined, ...params, index, From b05602b0b74d2e604421b54dc4d61bf5f72d33cd Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sat, 7 Oct 2023 11:50:13 -0400 Subject: [PATCH 088/256] chore(fix): don't spread `params` if used as `params.value` --- api/ipc.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/api/ipc.js b/api/ipc.js index 50650ca838..1f70043b7a 100644 --- a/api/ipc.js +++ b/api/ipc.js @@ -1005,16 +1005,16 @@ const { toString } = Object.prototype class IPCSearchParams extends URLSearchParams { constructor (params, nonce) { - const index = params?.index ?? globalThis.__args?.index ?? 0 - const seq = 'R' + nextSeq++ - + let value + if (params !== undefined && toString.call(params) !== '[object Object]') { + value = params + params = null + } super({ - value: params !== undefined && toString.call(params) !== '[object Object]' - ? params - : undefined, + value, + index: globalThis.__args?.index ?? 0, ...params, - index, - seq, + seq: 'R' + nextSeq++, nonce }) } From c9b3ee2fd9c1be48bdde24a8bbe1df29341cd8e1 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sat, 7 Oct 2023 11:56:47 -0400 Subject: [PATCH 089/256] chore(fix): set `nonce` conditionally --- api/ipc.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/ipc.js b/api/ipc.js index 1f70043b7a..aee0720829 100644 --- a/api/ipc.js +++ b/api/ipc.js @@ -1014,9 +1014,11 @@ class IPCSearchParams extends URLSearchParams { value, index: globalThis.__args?.index ?? 0, ...params, - seq: 'R' + nextSeq++, - nonce + seq: 'R' + nextSeq++ }) + if (nonce) { + this.set('nonce', nonce) + } } toString () { From 0f0505f20019fed98273450a0fc25c3501fd4445 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sun, 8 Oct 2023 19:48:38 -0400 Subject: [PATCH 090/256] chore: generate docs/typings --- api/README.md | 4 ++-- api/index.d.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api/README.md b/api/README.md index 9376770bb1..bcf344c107 100644 --- a/api/README.md +++ b/api/README.md @@ -1107,7 +1107,7 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesw import { send } from 'socket:ipc' ``` -## [`emit(name, value, target, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1065) +## [`emit(name, value, target, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1082) Emit event to be dispatched on `window` object. @@ -1118,7 +1118,7 @@ Emit event to be dispatched on `window` object. | target | EventTarget | window | true | | | options | Object | | true | | -## [`send(command, value, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1124) +## [`send(command, value, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1141) Sends an async IPC command request with parameters. diff --git a/api/index.d.ts b/api/index.d.ts index d1afb94554..bb227d0d07 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -2790,12 +2790,12 @@ declare module "socket:ipc" { * Sends a synchronous IPC command over XHR returning a `Result` * upon success or error. * @param {string} command - * @param {object|string?} [params] + * @param {any?} [value] * @param {object?} [options] * @return {Result} * @ignore */ - export function sendSync(command: string, params?: object | (string | null), options?: object | null): Result; + export function sendSync(command: string, value?: any | null, options?: object | null): Result; /** * Emit event to be dispatched on `window` object. * @param {string} name @@ -2824,21 +2824,21 @@ declare module "socket:ipc" { /** * Sends an async IPC command request with parameters and buffered bytes. * @param {string} command - * @param {object=} params + * @param {any=} value * @param {(Buffer|Uint8Array|ArrayBuffer|string|Array)=} buffer * @param {object=} options * @ignore */ - export function write(command: string, params?: object | undefined, buffer?: (Buffer | Uint8Array | ArrayBuffer | string | any[]) | undefined, options?: object | undefined): Promise<any>; + export function write(command: string, value?: any | undefined, buffer?: (Buffer | Uint8Array | ArrayBuffer | string | any[]) | undefined, options?: object | undefined): Promise<any>; /** * Sends an async IPC command request with parameters requesting a response * with buffered bytes. * @param {string} command - * @param {object=} params + * @param {any=} value * @param {object=} options * @ignore */ - export function request(command: string, params?: object | undefined, options?: object | undefined): Promise<any>; + export function request(command: string, value?: any | undefined, options?: object | undefined): Promise<any>; /** * Factory for creating a proxy based IPC API. * @param {string} domain From 468184f60535c7b2a50a9d03721983465bcc859e Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Mon, 18 Sep 2023 17:47:08 -0400 Subject: [PATCH 091/256] fix(ipc): stringify the value passed to `process.write` The value is expected by socket-node to be valid JSON: https://github.com/socketsupply/socket/blob/7bcf246bbaf96d93447df41e3c851d36ec3fd65b/npm/packages/@socketsupply/socket-node/index.js#L123 --- api/window.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/window.js b/api/window.js index f0c3f08a0d..d0da6cb888 100644 --- a/api/window.js +++ b/api/window.js @@ -288,7 +288,7 @@ export class ApplicationWindow { return await ipc.send('process.write', { index: this.#senderWindowIndex, event: options.event, - value: value ?? '' + value: value !== undefined ? JSON.stringify(value) : null }) } From d54a8b3302f9a7ed5c0f8eccae1dcd6f4c06ae3d Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:18:11 -0400 Subject: [PATCH 092/256] fix(extension): avoid precision loss from size_t -> uint --- include/socket/extension.h | 2 +- src/extension/ipc.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/socket/extension.h b/include/socket/extension.h index 3d3659cc3a..5b86c876cd 100644 --- a/include/socket/extension.h +++ b/include/socket/extension.h @@ -1055,7 +1055,7 @@ extern "C" { * @return The size of the IPC result bytes */ SOCKET_RUNTIME_EXTENSION_EXPORT - unsigned int sapi_ipc_result_get_bytes_size ( + size_t sapi_ipc_result_get_bytes_size ( const sapi_ipc_result_t* result ); diff --git a/src/extension/ipc.cc b/src/extension/ipc.cc index 0ebc9a2cc9..6373648ee7 100644 --- a/src/extension/ipc.cc +++ b/src/extension/ipc.cc @@ -596,7 +596,7 @@ unsigned char* sapi_ipc_result_get_bytes ( : nullptr; } -unsigned int sapi_ipc_result_get_bytes_size ( +size_t sapi_ipc_result_get_bytes_size ( const sapi_ipc_result_t* result ) { return result ? result->post.length : 0; From efec933234209fedee2892d3d25771cb17f0a82a Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:17:00 -0400 Subject: [PATCH 093/256] fix(ipc): omit `value` parameter if value is undefined --- api/ipc.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/ipc.js b/api/ipc.js index aee0720829..0d747ee045 100644 --- a/api/ipc.js +++ b/api/ipc.js @@ -1011,11 +1011,13 @@ class IPCSearchParams extends URLSearchParams { params = null } super({ - value, index: globalThis.__args?.index ?? 0, ...params, seq: 'R' + nextSeq++ }) + if (value !== undefined) { + this.set('value', value) + } if (nonce) { this.set('nonce', nonce) } From 8730cede65bdf955cdb5694154839d0ac102b4e8 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sat, 7 Oct 2023 00:26:59 -0400 Subject: [PATCH 094/256] feat(cli): allow subcommands, variables, and relative paths in extension compiler/linker flags --- src/cli/cli.cc | 55 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index b52eb5e587..414df4cb3b 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -4600,16 +4600,69 @@ int main (const int argc, const char* argv[]) { } if (fs::exists(target)) { + target = fs::canonical(target); + auto configFile = target / "socket.ini"; auto config = parseINI(fs::exists(configFile) ? readFile(configFile) : ""); settings["build_extensions_" + extension + "_path"] = target.string(); for (const auto& entry : config) { if (entry.first.starts_with("extension_sources")) { - settings["build_extensions_" + extension] += entry.second; + settings["build_extensions_" + extension] += fs::canonical(target / entry.second); } else if (entry.first.starts_with("extension_")) { auto key = replace(entry.first, "extension_", extension + "_"); auto value = entry.second; + if (key.ends_with("_flags")) { + // Replace all $(โ€ฆ) with evaluated stdout. + // E.g. $(pkg-config --libs --cflags libssl) => -lssl + auto match = std::smatch{}; + while (std::regex_search(value, match, std::regex("\\$\\((.*?)\\)"))) { + auto subcommand = match[1].str(); + log("Running subcommand: " + subcommand); + auto proc = exec(subcommand); + if (proc.exitCode != 0) { + log("ERROR: failed to run subcommand: " + subcommand); + exit(proc.exitCode); + } + auto output = trim(replace(proc.output, "\n", " ")); + value = value.replace(match[0].first, match[0].second, output); + } + // Replace all $\w+ with env var. + // E.g. $CXX => clang++ + match = std::smatch{}; + while (std::regex_search(value, match, std::regex("\\$(\\w+)"))) { + auto envVar = match[1].str(); + auto envValue = envVar == "PWD" + ? target.string() // Use the extension root. + : getEnv(envVar); + if (envValue.size() == 0) { + log("ERROR: failed to find env var: " + envVar); + exit(1); + } + value = value.replace(match[0].first, match[0].second, envValue); + } + // Replace all ./ and ../ with absolute paths. + match = std::smatch{}; + while (std::regex_search(value, match, std::regex("\\.\\.?/([^ ]|\\\\ )+"))) { + auto relativePath = match[0].str(); + auto absolutePath = fs::absolute(target / relativePath); + try { + absolutePath = fs::canonical(absolutePath); + value = value.replace( + match[0].first, + match[0].second, + absolutePath.string() + ); + } catch (const std::filesystem::filesystem_error& e) { + if (e.code() == std::errc::no_such_file_or_directory) { + log("ERROR: path not found: " + absolutePath.string()); + exit(1); + } else { + throw e; + } + } + } + } auto index = "build_extensions_" + key; if (settings[index].size() > 0) { settings[index] += " " + value; From 2759a642a87c6c3e4d105e33ebceebf261ec9061 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Mon, 9 Oct 2023 10:03:38 -0400 Subject: [PATCH 095/256] fix: quiet subcommand log unless DEBUG/VERBOSE=1 --- src/cli/cli.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 414df4cb3b..7ab7ee3638 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -4618,7 +4618,9 @@ int main (const int argc, const char* argv[]) { auto match = std::smatch{}; while (std::regex_search(value, match, std::regex("\\$\\((.*?)\\)"))) { auto subcommand = match[1].str(); - log("Running subcommand: " + subcommand); + if (getEnv("DEBUG") == "1" || getEnv("VERBOSE") == "1") { + log("Running subcommand: " + subcommand); + } auto proc = exec(subcommand); if (proc.exitCode != 0) { log("ERROR: failed to run subcommand: " + subcommand); From e9d5fcf5371568633e72cbb74817cdaf0dc21b48 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 08:04:46 -0400 Subject: [PATCH 096/256] feat(src/config.hh): introduce 'config.hh' --- src/config.hh | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/config.hh diff --git a/src/config.hh b/src/config.hh new file mode 100644 index 0000000000..1c7f829fa4 --- /dev/null +++ b/src/config.hh @@ -0,0 +1,40 @@ +#ifndef SSC_CONFIG_H +#define SSC_CONFIG_H + +#ifndef DEBUG +#define DEBUG 0 +#endif + +#ifndef SSC_SETTINGS +#define SSC_SETTINGS "" +#endif + +#ifndef SSC_VERSION +#define SSC_VERSION "" +#endif + +#ifndef SSC_VERSION_HASH +#define SSC_VERSION_HASH "" +#endif + +#ifndef HOST +#define HOST "localhost" +#endif + +#ifndef PORT +#define PORT 0 +#endif + +#if defined(__cplusplus) +#include <map> + +namespace SSC { + // from init.cc + extern const std::map<std::string, std::string> getUserConfig (); + extern bool isDebugEnabled (); + extern const char* getDevHost (); + extern int getDevPort (); +} +#endif + +#endif From bb6faf6f3b0194e7aaa2573c3265568abd6873eb Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 08:05:06 -0400 Subject: [PATCH 097/256] refactor(src/common.hh): move user config functions to 'config.hh' --- src/common.hh | 33 ++------------------------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/src/common.hh b/src/common.hh index 14daecdac5..48ef668f6b 100644 --- a/src/common.hh +++ b/src/common.hh @@ -1,6 +1,8 @@ #ifndef SSC_CORE_COMMON_H #define SSC_CORE_COMMON_H +#include "config.hh" + // macOS/iOS #if defined(__APPLE__) #include <TargetConditionals.h> @@ -139,31 +141,6 @@ static os_log_t SSC_OS_LOG_DEBUG_BUNDLE = nullptr; #include <thread> #include <vector> -#ifndef DEBUG -#define DEBUG 0 -#endif - -#ifndef SSC_SETTINGS -#define SSC_SETTINGS "" -#endif - -#ifndef SSC_VERSION -#define SSC_VERSION "" -#endif - -#ifndef SSC_VERSION_HASH -#define SSC_VERSION_HASH "" -#endif - -#ifndef HOST -#define HOST "localhost" -#endif - -#ifndef PORT -#define PORT 0 -#endif - - #if defined(_WIN32) #define SHARED_OBJ_EXT ".dll" #else @@ -221,12 +198,6 @@ namespace SSC { inline const auto DEFAULT_SSC_RC_FILENAME = String(".sscrc"); inline const auto DEFAULT_SSC_ENV_FILENAME = String(".ssc.env"); - // from init.cc - extern const Map getUserConfig (); - extern bool isDebugEnabled (); - extern const char* getDevHost (); - extern int getDevPort (); - inline String encodeURIComponent (const String& sSrc); inline String decodeURIComponent (const String& sSrc); inline String trim (String str); From a5222c2e0f9e91c7b27e3aafc70f2070a07df446 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 08:05:34 -0400 Subject: [PATCH 098/256] refactor(src/init.cc): use 'config.hh' --- src/init.cc | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/init.cc b/src/init.cc index af173fa7f5..9100807b85 100644 --- a/src/init.cc +++ b/src/init.cc @@ -1,12 +1,14 @@ -#include "common.hh" +#include "config.hh" +#if defined(__cplusplus) +#include <map> // These rely on project-specific, compile-time variables. namespace SSC { bool isDebugEnabled () { return DEBUG == 1; } - const Map getUserConfig () { + const std::map<std::string, std::string> getUserConfig () { #include "user-config-bytes.hh" // NOLINT return parseINI(std::string( (const char*) __ssc_config_bytes, @@ -23,3 +25,4 @@ namespace SSC { return PORT; } } +#endif From 323305a7d9222c78fc038676fb94f72774941cb3 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 08:06:01 -0400 Subject: [PATCH 099/256] refactor(cli): include 'config.hh' in android build --- src/cli/cli.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 7ab7ee3638..94d2a711e1 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -2825,6 +2825,7 @@ int main (const int argc, const char* argv[]) { // Core fs::copy(trim(prefixFile("src/common.hh")), jni, fs::copy_options::overwrite_existing); + fs::copy(trim(prefixFile("src/config.hh")), jni, fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/init.cc")), jni, fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/android/bridge.cc")), jni / "android", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/android/runtime.cc")), jni / "android", fs::copy_options::overwrite_existing); @@ -2833,7 +2834,6 @@ int main (const int argc, const char* argv[]) { fs::copy(trim(prefixFile("src/core/core.hh")), jni / "core", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/core/json.hh")), jni / "core", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/core/preload.hh")), jni / "core", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/core/preload.cc")), jni / "core", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/ipc/ipc.hh")), jni / "ipc", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/window/options.hh")), jni / "window", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/window/window.hh")), jni / "window", fs::copy_options::overwrite_existing); From e35936196760b390391349de0ab64a6b46c31358 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 16:01:50 -0400 Subject: [PATCH 100/256] refactor(api/internal/permissions.js): add 'Symbol.toStringTag' --- api/internal/permissions.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/internal/permissions.js b/api/internal/permissions.js index 60f8129762..048cfed12c 100644 --- a/api/internal/permissions.js +++ b/api/internal/permissions.js @@ -10,8 +10,6 @@ class PermissionStatus extends EventTarget { #state = null #name = null - // eslint-disable-next-line - [Symbol.toStringTag] = 'PermissionStatus' constructor (name, subscribe) { super() @@ -24,6 +22,10 @@ class PermissionStatus extends EventTarget { }) } + get [Symbol.toStringTag] () { + return 'PermissionStatus' + } + get name () { return this.#name } From 37f8505c876db4fde45b29afb20af97512984002 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 4 Oct 2023 16:18:13 -0400 Subject: [PATCH 101/256] feat(api/notification): introduce notification API --- api/notification.js | 307 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 api/notification.js diff --git a/api/notification.js b/api/notification.js new file mode 100644 index 0000000000..cda189f926 --- /dev/null +++ b/api/notification.js @@ -0,0 +1,307 @@ +import { Enumeration } from './enumeration.js' +import language from './language.js' +import location from './location.js' +import URL from './url.js' + +const isServiceWorkerGlobalScope = typeof globalThis.registration?.active === 'string' + +export const NotificationDirection = new Enumeration(['auto', 'ltr', 'rtl']) + +export class NotificationAction { + #action = null + #title = null + #icon = null + constructor (options) { + if (options?.action === undefined) { + throw new TypeError( + 'Failed to read the \'action\' property from ' + `'NotificationAction': Required member is ${options.action}.` + ) + } + + if (options?.title === undefined) { + throw new TypeError( + 'Failed to read the \'title\' property from ' + `'NotificationAction': Required member is ${options.title}.` + ) + } + + this.#action = String(options.action) + this.#title = String(options.title) + this.#icon = String(options.icon ?? '') + } + + get action () { return this.#action } + get title () { return this.#title } + get icon () { return this.#icon } +} + +export class NotificationOptions { + #actions = [] + #badge = '' + #body = '' + #data = null + #dir = 'auto' + #icon = '' + #lang = '' + #renotify = false + #requireInteraction = false + #silent = false + #tag = '' + #vibrate = [] + + constructor (options) { + if ('dir' in options) { + if (!(options.dir in NotificationDirection)) { + throw new TypeError( + 'Failed to read the \'dir\' property from \'NotificationOptions\': ' + + `The provided value '${options.dir}' is not a valid enum value of ` + + 'type NotificationDirection.' + ) + } + + this.#dir = options.dir + } + + if ('actions' in options) { + if (!(Symbol.iterator in options.actions)) { + throw new TypeError( + 'Failed to read the \'actions\' property from ' + + '\'NotificationOptions\': The object must have a callable ' + + '@@iterator property.' + ) + } + + for (const action of options.actions) { + try { + this.#actions.push(new NotificationAction(action)) + } catch (err) { + throw new TypeError( + 'Failed to read the \'actions\' property from ' + + `'NotificationOptions': ${err.message}` + ) + } + } + } + + if (this.#actions.length && !isServiceWorkerGlobalScope) { + throw new TypeError( + 'Failed to construct \'Notification\': Actions are only supported ' + + 'for persistent notifications shown using ' + + 'ServiceWorkerRegistration.showNotification().' + ) + } + + if ('badge' in options && options.badge !== undefined) { + this.#badge = String(new URL(String(options.badge), location.href)) + } + + if ('body' in options && options.body !== undefined) { + this.#body = String(options.body) + } + + if ('data' in options && options.data !== undefined) { + this.#data = options.data + } + + if ('icon' in options && options.icon !== undefined) { + this.#icon = String(new URL(String(options.icon), location.href)) + } + + if ('image' in options && options.image !== undefined) { + this.#image = String(new URL(String(options.image), location.href)) + } + + if ('lang' in options && options.lang !== undefined) { + if (typeof options.lang === 'string' && options.lang.length > 2) { + this.#lang = language.describe(options.lang).[0]?.tag || '' + } + } + + if ('tag' in options && options.tag !== undefined) { + this.#tag = String(options.tag) + } + + if ('renotify' in options && options.renotify !== undefined) { + this.#renotify = Boolean(options.renotify) + + if (this.#renotify === true && !this.#tag.length) { + throw new TypeError( + 'Notifications which set the renotify flag must specify a non-empty tag.' + ) + } + } + + if ('requireInteraction' in options && options.requireInteraction !== undefined) { + this.#requireInteraction = Boolean(options.requireInteraction) + } + + if ('silent' in options && options.silent !== undefined) { + this.#silent = Boolean(options.silent) + } + + if ('vibrate' in options && options.vibrate !== undefined) { + if (Array.isArray(options.vibrate)) { + this.#vibrate = this.#vibrate + } else if (options.vibrate) { + this.#vibrate = [options.vibrate] + } else { + this.#vibrate = [0] + } + + if (this.#vibrate.length) { + throw new TypeError( + 'Silent notifications must not specify vibration patterns.' + ) + } + + this.#vibrate = this.#vibrate + .map((v) => parseInt(v) || 0) + .map((v) => Math.min(v, 10000)) + .map((v) => v < 0 ? 10000 : v) + } + } + + get actions () { return this.#actions } + get badge () { return this.#badge } + get body () { return this.#body } + get data () { return this.#data } + get dir () { return this.#dir } + get icon () { return this.#icon } + get lang () { return this.#lang } + get renotify () { return this.#renotify } + get requireInteraction () { return this.#requireInteraction } + get silent () { return this.#silent } + get tag () { return this.#tag } + get vibrate () { return this.#vibrate } +} + +export class Notification extends EventTarget { + static get permission () { + } + + #onclick = null + #onclose = null + #onerror = null + #onshow = null + + #options = null + #timestamp = Date.now() + #title = null + + constructor (title, options = {}) { + super() + if (arguments.length === 0) { + throw new TypeError( + 'Failed to construct \'Notification\': ' + + '1 argument required, but only 0 present.' + ) + } + + if (options === null || options === undefined) { + options = {} + } + + this.#title = String(title) + + if (typeof options !== 'object') { + throw new TypeError( + 'Failed to construct \'Notification\': ' + + 'The provided value is not of type \'NotificationOptions\'.' + ) + } + + try { + this.#options = new NotificationOptions(options) + } catch (err) { + throw new TypeError( + `Failed to construct 'Notification': ${err.message}` + ) + } + } + + get onclick () { return this.#onclick } + set onclick (onclick) { + if (this.#onclick === onclick) { + return + } + + if (this.#onclick) { + this.removeEventListener('click', this.#onclick) + this.#onclick = null + } + + if (typeof onclick === 'function') { + this.#onclick = onclick + this.addEventListener('click', onclick) + } + } + + get onclose () { return this.#onclose } + set onclose (onclose) { + if (this.#onclose === onclose) { + return + } + + if (this.#onclose) { + this.removeEventListener('close', this.#onclose) + this.#onclose = null + } + + if (typeof onclose === 'function') { + this.#onclose = onclose + this.addEventListener('close', onclose) + } + } + + get onerror () { return this.#onerror } + set onerror (onerror) { + if (this.#onerror === onerror) { + return + } + + if (this.#onerror) { + this.removeEventListener('error', this.#onerror) + this.#onerror = null + } + + if (typeof onerror === 'function') { + this.#onerror = onerror + this.addEventListener('error', onerror) + } + } + + get onshow () { return this.#onshow } + set onshow (onshow) { + if (this.#onshow === onshow) { + return + } + + if (this.#onshow) { + this.removeEventListener('show', this.#onshow) + this.#onshow = null + } + + if (typeof onshow === 'function') { + this.#onshow = onshow + this.addEventListener('show', onshow) + } + } + + get actions () { return this.#options.actions } + get badge () { return this.#options.badge } + get body () { return this.#options.body } + get data () { return this.#options.data } + get dir () { return this.#options.dir } + get icon () { return this.#options.icon } + get lang () { return this.#options.lang } + get renotify () { return this.#options.renotify } + get requireInteraction () { return this.#options.requireInteraction } + get silent () { return this.#silent } + get tag () { return this.#options.tag } + get timestamp () { return this.#timestamp } + get title () { return this.#title } + get vibrate () { return this.#options.vibrate } +} + +export default Notification From 328c94335ae5b63bbfcd7b032c699310e381dbe3 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 12:24:33 -0400 Subject: [PATCH 102/256] refactor(src/ipc/ipc.hh): introduce notification center delegate --- src/ipc/ipc.hh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/ipc/ipc.hh b/src/ipc/ipc.hh index 7c7f367a64..d5f986e92d 100644 --- a/src/ipc/ipc.hh +++ b/src/ipc/ipc.hh @@ -77,6 +77,8 @@ namespace SSC::IPC { - (void) locationManager: (CLLocationManager*) locationManager didVisit: (CLVisit*) visit; +- (void) locationManager: (CLLocationManager*) locationManager + didChangeAuthorizationStatus: (CLAuthorizationStatus) status; - (void) locationManagerDidChangeAuthorization: (CLLocationManager*) locationManager; @end @@ -94,7 +96,7 @@ namespace SSC::IPC { @property (atomic, retain) NSMutableArray* locationRequestCompletions; @property (atomic, retain) NSMutableArray* locationWatchers; @property (nonatomic) SSC::IPC::Router* router; -@property (atomic, assign) BOOL isActivated; +@property (atomic, assign) BOOL isAuthorized; - (BOOL) attemptActivation; - (BOOL) attemptActivationWithCompletion: (void (^)(BOOL)) completion; - (BOOL) getCurrentPositionWithCompletion: (void (^)(NSError*, CLLocation*)) completion; @@ -102,6 +104,16 @@ namespace SSC::IPC { completion: (void (^)(NSError*, CLLocation*)) completion; - (BOOL) clearWatch: (NSInteger) identifier; @end + +@interface SSCUserNotificationCenterDelegate : NSObject<UNUserNotificationCenterDelegate> +- (void) userNotificationCenter: (UNUserNotificationCenter*) center + didReceiveNotificationResponse: (UNNotificationResponse*) response + withCompletionHandler: (void (^)(void)) completionHandler; + +- (void) userNotificationCenter: (UNUserNotificationCenter*) center + willPresentNotification: (UNNotification*) notification + withCompletionHandler: (void (^)(UNNotificationPresentationOptions options)) completionHandler; +@end #endif namespace SSC::IPC { @@ -233,6 +245,7 @@ namespace SSC::IPC { SSCLocationObserver* locationObserver = nullptr; SSCIPCSchemeHandler* schemeHandler = nullptr; SSCIPCSchemeTasks* schemeTasks = nullptr; + NSTimer* notificationPollTimer = nullptr; #endif Router (); From 89be7b0b81a07508f2d5a1e13d950065bcc2546c Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 12:24:56 -0400 Subject: [PATCH 103/256] refactor(src/ipc/ipc.cc): 'Message::has()' should check for length --- src/ipc/ipc.cc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ipc/ipc.cc b/src/ipc/ipc.cc index ecf1bf35ac..7656275c2a 100644 --- a/src/ipc/ipc.cc +++ b/src/ipc/ipc.cc @@ -106,7 +106,10 @@ namespace SSC::IPC { } bool Message::has (const String& key) const { - return this->args.find(key) != this->args.end(); + return ( + this->args.find(key) != this->args.end() && + this->args.at(key).size() > 0 + ); } String Message::get (const String& key) const { From 48df9a38ad01d410fee590db323d9d15998c467a Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:36:34 -0400 Subject: [PATCH 104/256] refactor(src/cli/templates.hh): cli help cleanup, permissions, usages, upgrade gradle/kotlin --- src/cli/templates.hh | 565 +++++++++++++++++++++++++++++-------------- 1 file changed, 377 insertions(+), 188 deletions(-) diff --git a/src/cli/templates.hh b/src/cli/templates.hh index 80a76664a0..057223fe5e 100644 --- a/src/cli/templates.hh +++ b/src/cli/templates.hh @@ -9,21 +9,19 @@ usage: ssc [SUBCOMMAND] -h subcommands: - build build project - list-devices get the list of connected devices - init create a new project (in the current directory) - install-app install app to the device - print-build-dir print build path to stdout - run run application - env print relavent environment variables - setup install build dependencies + build build project + list-devices get the list of connected devices + init create a new project (in the current directory) + install-app install app to the device + print-build-dir print build path to stdout + run run application + env print relavent environment variables + setup install build dependencies general options: - -h, --help print help message - -v, --version print program version - --prefix print install path - --debug enable debug mode - --verbose enable verbose output + -h, --help print help message + --prefix print install path + -v, --version print program version )TEXT"; constexpr auto gHelpTextBuild = R"TEXT( @@ -33,25 +31,43 @@ usage: ssc build [options] [<project-dir>] options: - --platform=<platform> target (android, android-emulator, ios, ios-simulator) - --port=<port> load "index.html" from "http://localhost:<port>" - -o, --only-build only run build step, - -r, --run run after building - -w, --watch watch for changes to rerun build step - --headless run headlessly - --stdin read from stdin (emitted in window 0) - --test[=path] indicate test mode, optionally importing a test file - -options: - --prod disable debugging info, inspector, etc. - - -p package the app - -c code sign - -s prep for App Store - -macOS-specific options: - -e specify entitlements - -n notarize + --platform=<platform> platform target to build application for (defaults to host): + - android + - android-emulator + - ios + - ios-simulator + --port=<port> load "index.html" from "http://localhost:<port>" + --test[=path] indicate test mode, optionally importing a test file relative to resource files + --headless build application to run in headless mode (without frame or window) + --prod build for production (disables debugging info, inspector, etc.) + --stdin read from stdin (dispacted as 'process.stdin' event to window #0) + -D, --debug enable debug mode + -o, --only-build only run build step, + -p, --package package the app for distribution + -q, --quiet hint for less log output + -r, --run run after building + -V, --verbose enable verbose output + -w, --watch watch for changes to rerun build step + +Linux options: + -f, --package-format=<format> package a Linux application in a specified format for distribution: + - deb (default) + - zip + +macOS options: + -c code sign application with 'codesign' + -n notarize application with 'notarytool' + -f, --package-format=<format> package a macOS application in a specified format for distribution: + - zip (default) + - pkg + +iOS options: + -c code sign application during xcoddbuild + (requires '[ios] provisioning_profile' in 'socket.ini') + +Windows options: + -f, --package-format=<format> package a Windows application in a specified format for distribution: + - appx (default) )TEXT"; constexpr auto gHelpTextListDevices = R"TEXT( @@ -63,10 +79,12 @@ usage: ssc list-devices [options] --platform=<platform> options: - --platform android|ios - --ecid show device ECID (ios only) - --udid show device UDID (ios only) - --only only show ECID or UDID of the first device + --platform=<platform> platform target to list devices for: + - android + - ios + --ecid show device ECID (ios only) + --udid show device UDID (ios only) + --only only show ECID or UDID of the first device (ios only) )TEXT"; constexpr auto gHelpTextInit = R"TEXT( @@ -78,22 +96,31 @@ usage: ssc init [<project-dir>] options: - --config only create the config file - --name project name + -C, --config only create the config file + -n, --name project name )TEXT"; constexpr auto gHelpTextInstallApp = R"TEXT( ssc v{{ssc_version}} -Install the app to the device. We only support iOS at the moment. +Install the app to the device or host target. usage: - ssc install-app --platform=<platform> [--device=<identifier>] + ssc install-app [--platform=<platform>] [--device=<identifier>] [options] options: - --platform=<platform> android|ios - --device=<identifier> identifier (ecid, ID) of the device to install to - if not specified, tries to run on the current device + -D, --debug enable debug output + --device[=identifier] identifier (ecid, ID) of the device to install to + if not specified, tries to run on the current device + --platform=<platform> platform to install application to device (defaults to host):: + - android + - ios + --prod install production application + -V, --verbose enable verbose output + +macOS options: + --target=<target> installation target for macOS application (defaults to '/') + the application is installed into '$target/Applications' )TEXT"; constexpr auto gHelpTextPrintBuildDir = R"TEXT( @@ -102,11 +129,16 @@ ssc v{{ssc_version}} Create a new project (in the current directory) usage: - ssc print-build-dir [--platform=<platform>] [--prod] [<project-dir>] + ssc print-build-dir [--platform=<platform>] [--prod] [--root] [<project-dir>] options: - --platform android|android-emulator|ios|ios-simulator; if not specified, runs on the current platform - --root print root build directory + --platform platform to print build directory for (defaults to host): + - android + - android-emulator + - ios + - ios-simulator + --prod indicate production build directory + --root print the root build directory )TEXT"; constexpr auto gHelpTextRun = R"TEXT( @@ -116,24 +148,34 @@ usage: ssc run [options] [<project-dir>] options: - --platform android|ios|ios-simulator; if not specified, runs on the current platform - --prod run production build - --test=path indicate test mode + -D, --debug enable debug mode + --headless run application in headless mode (without frame or window) + --platform=<platform> platform target to run application on (defaults to host): + - android + - android-emulator + - ios + - ios-simulator + --prod build for production (disables debugging info, inspector, etc.) + --test[=path] indicate test mode, optionally importing a test file relative to resource files + -V, --verbose enable verbose output )TEXT"; constexpr auto gHelpTextSetup = R"TEXT( ssc v{{ssc_version}} -Setup build tools for target <platform> +Setup build tools for host or target platform. Platforms not listed below can be setup using instructions at https://socketsupply.co/guides usage: - ssc setup [options] --platform=<platform> + ssc setup [options] --platform=<platform> [-y|--yes] options: - --platform android|linux|windows - -y, --yes answer yes to any prompts + --platform=<platform> platform target to run setup for (defaults to host): + - android + - ios + -q, --quiet hint for less log output + -y, --yes answer yes to any prompts )TEXT"; @@ -169,135 +211,203 @@ constexpr auto gHelloWorld = R"HTML( )HTML"; // -// Darwin config +// macOS 'Info.plist' file // -constexpr auto gPListInfo = R"XML(<?xml version="1.0" encoding="UTF-8"?> +constexpr auto gMacOSInfoPList = R"XML(<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> + <!--- Metadata --> + <key>CFBundleDisplayName</key> + <string>{{build_name}}</string> + <key>CFBundleName</key> <string>{{build_name}}</string> - <key>DTSDKName</key> - <string>macosx10.13</string> - <key>DTXcode</key> - <string>0941</string> - <key>NSHumanReadableCopyright</key> - <string>{{meta_copyright}}</string> - <key>DTSDKBuild</key> - <string>10.13</string> + + <key>CFBundleIconFile</key> + <string>icon.icns</string> + + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleVersion</key> <string>{{meta_version}}</string> - <key>BuildMachineOSBuild</key> - <string>17D102</string> - <key>NSCameraUsageDescription</key> - <string>This app needs access to the camera</string> - <key>NSBluetoothAlwaysUsageDescription</key> - <string>The app would like to discover and connect to peers</string> - <key>NSLocationUsageDescription</key> - <string>{{meta_title}} would like access to your location</string> - - <key>NSLocationWhenInUseUsageDescription</key> - <string>{{meta_title}} would like access to your location while open</string> + <key>CFBundleShortVersionString</key> + <string>{{meta_version}}</string> - <key>NSLocationAlwaysAndWhenInUseUsageDescription</key> - <string>{{meta_title}} would like access to your location</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> - <key>NSSpeechRecognitionUsageDescription</key> - <string>{{meta_title}} would like to access Speech Recognition</string> + <key>CFBundleExecutable</key> + <string>{{build_name}}</string> - <key>NSMicrophoneUsageDescription</key> - <string>{{meta_title}} would like to access to your microphone</string> + <key>CFBundleIdentifier</key> + <string>{{meta_bundle_identifier}}</string> - <key>NSCameraUsageDescription</key> - <string>{{meta_title}} would like to access to your camera</string> + <key>LSApplicationCategoryType</key> + <string>{{mac_category}}</string> - <key>NSMotionUsageDescription</key> - <string>{{meta_title}} would like to access to detect your device motion</string> + <key>NSHumanReadableCopyright</key> + <string>{{meta_copyright}}</string> <key>NSMainNibFile</key> <string>MainMenu</string> + + <key>NSPrincipalClass</key> + <string>AtomApplication</string> + + + <!-- Application configuration --> + <key>NSLocationDefaultAccuracyReduced</key> + <true/> + + <key>LSMinimumSystemVersion</key> + <string>{{mac_minimum_supported_version}}</string> + <key>LSMultipleInstancesProhibited</key> <true/> - <key>CFBundlePackageType</key> - <string>APPL</string> - <key>CFBundleIconFile</key> - <string>icon.icns</string> - <key>CFBundleShortVersionString</key> - <string>{{meta_version}}</string> + <key>NSHighResolutionCapable</key> <true/> + + <key>NSRequiresAquaSystemAppearance</key> + <false/> + + <key>NSSupportsAutomaticGraphicsSwitching</key> + <true/> + + <key>SoftResourceLimits</key> + <dict> + <key>NumberOfFiles</key> + <integer>{{meta_file_limit}}</integer> + </dict> + + <key>WKAppBoundDomains</key> + <array> + <string>localhost</string> + <string>{{meta_bundle_identifier}}</string> + </array> + + + <!-- Permission usage descriptions --> + <key>NSAppDataUsageDescription</key> + <string> + {{meta_title}} would like shared app data access + </string> + + <key>NSBluetoothAlwaysUsageDescription</key> + <string> + {{meta_title}} would like to discover and connect to peers using Bluetooth + </string> + + <key>NSCameraUsageDescription</key> + <string> + {{meta_title}} would like to access to your camera + </string> + + <key>NSLocationAlwaysUsageDescription</key> + <string> + {{meta_title}} would like access to your location + </string> + + <key>NSLocationWhenInUseUsageDescription</key> + <string> + {{meta_title}} would like access to your location when in use + </string> + + <key>NSLocationTemporaryUsageDescriptionDictionary</key> + <string> + {{meta_title}} would like temporary access to your location + </string> + <key>NSMicrophoneUsageDescription</key> - <string>This app needs access to the microphone</string> + <string> + {{meta_title}} would like to access to your microphone + </string> + + <key>NSSpeechRecognitionUsageDescription</key> + <string> + {{meta_title}} would like to access Speech Recognition + </string> + + <key>NSMotionUsageDescription</key> + <string> + {{meta_title}} would like to access to detect your device motion + </string> + + + <!-- Security configuration --> <key>NSAppTransportSecurity</key> <dict> + <key>NSAllowsArbitraryLoads</key> + <false/> + + <key>NSAllowsLocalNetworking</key> + <true/> + <key>NSExceptionDomains</key> <dict> <key>127.0.0.1</key> <dict> <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key> <true/> + <key>NSTemporaryExceptionRequiresForwardSecrecy</key> <false/> + <key>NSIncludesSubdomains</key> <false/> + <key>NSTemporaryExceptionMinimumTLSVersion</key> <string>1.0</string> + <key>NSTemporaryExceptionAllowsInsecureHTTPSLoads</key> <false/> </dict> + <key>localhost</key> <dict> <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key> <true/> + <key>NSTemporaryExceptionRequiresForwardSecrecy</key> <false/> + <key>NSIncludesSubdomains</key> <false/> + <key>NSTemporaryExceptionMinimumTLSVersion</key> <string>1.0</string> + <key>NSTemporaryExceptionAllowsInsecureHTTPSLoads</key> <false/> </dict> </dict> - <key>NSAllowsArbitraryLoads</key> - <false/> - <key>NSAllowsLocalNetworking</key> - <true/> </dict> - <key>CFBundleInfoDictionaryVersion</key> - <string>6.0</string> - <key>CFBundleExecutable</key> - <string>{{build_name}}</string> - <key>DTCompiler</key> - <string>com.apple.compilers.llvm.clang.1_0</string> - <key>NSPrincipalClass</key> - <string>AtomApplication</string> - <key>NSRequiresAquaSystemAppearance</key> - <false/> - <key>CFBundleIdentifier</key> - <string>{{meta_bundle_identifier}}</string> - <key>LSApplicationCategoryType</key> - <string>{{mac_category}}</string> + + + <!-- Debug information --> + <key>BuildMachineOSBuild</key> + <string>{{__xcode_macosx_sdk_build_version}}</string> + + <key>BuildMachineOSBuild</key> + <string>{{__xcode_macosx_sdk_build_version}}</string> + + <key>DTSDKName</key> + <string>macosx{{__xcode_macosx_sdk_version}}</string> + + <key>DTXcode</key> + <string>{{__xcode_version}}</string> + + <key>DTSDKBuild</key> + <string>{{__xcode_macosx_sdk_version}}</string> + <key>DTXcodeBuild</key> - <string>9F2000</string> - <key>LSMinimumSystemVersion</key> - <string>10.10.0</string> - <key>CFBundleDisplayName</key> - <string>{{build_name}}</string> - <key>NSSupportsAutomaticGraphicsSwitching</key> - <true/> - <key>SoftResourceLimits</key> - <dict> - <key>NumberOfFiles</key> - <integer>{{meta_file_limit}}</integer> - </dict> - <key>WKAppBoundDomains</key> - <array> - <string>localhost</string> - </array> + <string>{{__xcode_build_version}}</string> - {{mac_info_plist_data}} + <!-- User given plist data --> +{{mac_info_plist_data}} </dict> </plist> )XML"; @@ -321,7 +431,7 @@ constexpr auto gAndroidManifest = R"XML( > <uses-sdk android:minSdkVersion="26" - android:targetSdkVersion="33" + android:targetSdkVersion="34" /> <uses-permission android:name="android.permission.INTERNET" /> @@ -391,7 +501,7 @@ constexpr auto gWindowsAppManifest = R"XML(<?xml version="1.0" encoding="utf-8"? xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" IgnorableNamespaces="uap3 rescap" > - <Identity Name="{bundle_identifier}" + <Identity Name="{{meta_bundle_identifier}}" ProcessorArchitecture="neutral" Publisher="{{win_publisher}}" Version="{{win_version}}" @@ -760,6 +870,7 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! "DEBUG=1", "SSC_VERSION={{SSC_VERSION}}", "SSC_VERSION_HASH={{SSC_VERSION_HASH}}", + "WAS_CODESIGNED={{WAS_CODESIGNED}}" "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -847,7 +958,7 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! CODE_SIGN_IDENTITY = "{{ios_codesign_identity}}"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = "{{apple_team_id}}"; + DEVELOPMENT_TEAM = "{{apple_team_identifier}}"; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include"; @@ -897,7 +1008,7 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = "{{apple_team_id}}"; + DEVELOPMENT_TEAM = "{{apple_team_identifier}}"; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include"; @@ -970,7 +1081,7 @@ constexpr auto gXCodeExportOptions = R"XML(<?xml version="1.0" encoding="UTF-8"? <key>method</key> <string>{{ios_distribution_method}}</string> <key>teamID</key> - <string>{{apple_team_id}}</string> + <string>{{apple_team_identifier}}</string> <key>uploadBitcode</key> <true/> <key>compileBitcode</key> @@ -989,52 +1100,20 @@ constexpr auto gXCodeExportOptions = R"XML(<?xml version="1.0" encoding="UTF-8"? </dict> </plist>)XML"; -constexpr auto gXCodePlist = R"XML(<?xml version="1.0" encoding="UTF-8"?> +// +// iOS 'Info.plist' file +// +constexpr auto gIOSInfoPList = R"XML(<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> + <!--- Metadata --> <key>CFBundleIdentifier</key> <string>{{meta_bundle_identifier}}</string> + <key>CFBundleIconFile</key> <string>ui/icon.png</string> - <key>NSAppTransportSecurity</key> - <dict> - <key>NSAllowsArbitraryLoads</key> - <true/> - </dict> - <key>NSHighResolutionCapable</key> - <true/> - - <key>NSLocalNetworkUsageDescription</key> - <string>{{meta_title}} would like to discover and connect to peers</string> - <key>NSBluetoothAlwaysUsageDescription</key> - <string>{{meta_title}} would like to discover and connect to peers</string> - <key>NSBluetoothPeripheralUsageDescription</key> - <string>{{meta_title}} would like to discover and connect to peers</string> - - <key>NSLocationUsageDescription</key> - <string>{{meta_title}} would like access to your location</string> - <key>NSLocationWhenInUseUsageDescription</key> - <string>{{meta_title}} would like access to your location while open</string> - <key>NSLocationAlwaysAndWhenInUseUsageDescription</key> - <string>{{meta_title}} would like access to your location</string> - - <key>NSSpeechRecognitionUsageDescription</key> - <string>{{meta_title}} would like to access Speech Recognition</string> - - <key>NSMicrophoneUsageDescription</key> - <string>{{meta_title}} would like to access to your microphone</string> - - <key>NSCameraUsageDescription</key> - <string>{{meta_title}} would like to access to your camera</string> - - <key>NSMotionUsageDescription</key> - <string>{{meta_title}} would like to access to detect your device motion</string> - <key>NSRequiresAquaSystemAppearance</key> - <false/> - <key>NSSupportsAutomaticGraphicsSwitching</key> - <true/> <key>CFBundleURLTypes</key> <array> <dict> @@ -1046,17 +1125,115 @@ constexpr auto gXCodePlist = R"XML(<?xml version="1.0" encoding="UTF-8"?> </array> </dict> </array> + + <!-- Application configuration --> <key>LSApplicationQueriesSchemes</key> <array> <string>{{ios_protocol}}</string> </array> + + <key>NSHighResolutionCapable</key> + <true/> + + <key>NSLocationDefaultAccuracyReduced</key> + <false/> + + <key>NSRequiresAquaSystemAppearance</key> + <false/> + + <key>NSSupportsAutomaticGraphicsSwitching</key> + <true/> + <key>UIBackgroundModes</key> <array> <string>fetch</string> <string>processing</string> + <string>location</string> + <string>bluetooth-central</string> + <string>bluetooth-peripheral</string> + <string>remote-notification</string> </array> - {{ios_info_plist_data}} + <key>UIRequiredDeviceCapabilities</key> + <array> + <string>accelerometer</string> + <string>bluetooth-le</string> + <string>gps</string> + <string>gyroscope</string> + <string>location-services</string> + <string>microphone</string> + <string>peer-to-peer</string> + <string>video-camera</string> + </array> + + + <!-- Permission usage descriptions --> + <key>NSAppDataUsageDescription</key> + <string> + {{meta_title}} would like shared app data access + </string> + + <key>NSBluetoothAlwaysUsageDescription</key> + <string> + {{meta_title}} would like to discover and connect to peers using Bluetooth + </string> + + <key>NSCameraUsageDescription</key> + <string> + {{meta_title}} would like to access to your camera + </string> + + <key>NSLocalNetworkUsageDescription</key> + <string> + {{meta_title}} would like to discover and connect to peers using your local network + </string> + + <key>NSLocationAlwaysUsageDescription</key> + <string> + {{meta_title}} would like access to your location + </string> + + <key>NSLocationWhenInUseUsageDescription</key> + <string> + {{meta_title}} would like access to your location when in use + </string> + + <key>NSLocationAlwaysAndWhenInUseUsageDescription</key> + <string> + {{meta_title}} would like access to your location + </string> + + <key>NSLocationTemporaryUsageDescriptionDictionary</key> + <string> + {{meta_title}} would like temporary access to your location + </string> + + <key>NSMicrophoneUsageDescription</key> + <string> + {{meta_title}} would like to access to your microphone + </string> + + <key>NSSpeechRecognitionUsageDescription</key> + <string> + {{meta_title}} would like to access Speech Recognition + </string> + + <key>NSMotionUsageDescription</key> + <string> + {{meta_title}} would like to access to detect your device motion + </string> + + + <!-- Security configuration --> + <key>NSAppTransportSecurity</key> + <dict> + <key>NSAllowsArbitraryLoads</key> + <true/> + </dict> + + + <!-- User given plist data --> +{{ios_info_plist_data}} </dict> </plist>)XML"; @@ -1064,16 +1241,19 @@ constexpr auto gXcodeEntitlements = R"XML(<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> - <key>get-task-allow</key> - <{{apple_instruments}}/> - <key>com.apple.security.app-sandbox</key> - <true/> <key>com.apple.security.network.server</key> <true/> <key>com.apple.security.network.client</key> <true/> - <key>com.apple.security.device.bluetooth</key> + <key>com.apple.security.cs.allow-jit</key> + <true/> + <key>com.apple.security.files.user-selected.read-write</key> <true/> + <key>com.apple.security.inherit</key> + <true/> + + <!-- Generated entitlements given plist data --> +{{configured_entitlements}} </dict> </plist>)XML"; @@ -1082,7 +1262,7 @@ constexpr auto gXcodeEntitlements = R"XML(<?xml version="1.0" encoding="UTF-8"?> // constexpr auto gGradleBuild = R"GROOVY( buildscript { - ext.kotlin_version = '1.7.0' + ext.kotlin_version = '1.9.10' repositories { google() mavenCentral() @@ -1114,15 +1294,24 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { - compileSdkVersion 33 - ndkVersion "25.0.8775105" + compileSdkVersion 34 + ndkVersion "26.0.10792818" flavorDimensions "default" namespace '{{meta_bundle_identifier}}' + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = 17 + } + defaultConfig { applicationId "{{meta_bundle_identifier}}" minSdkVersion 26 - targetSdkVersion 33 + targetSdkVersion 34 versionCode {{meta_revision}} versionName "{{meta_version}}" @@ -1169,10 +1358,11 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' - implementation 'androidx.appcompat:appcompat:1.5.0' - implementation 'androidx.webkit:webkit:1.4.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.core:core-ktx:1.12.0' + implementation 'androidx.webkit:webkit:1.8.0' } )GROOVY"; @@ -1195,7 +1385,7 @@ org.gradle.parallel=true android.useAndroidX=true android.enableJetifier=true -android.suppressUnsupportedCompileSdk=33 +android.suppressUnsupportedCompileSdk=34 android.experimental.legacyTransform.forceNonIncremental=true kotlin.code.style=official @@ -1312,7 +1502,7 @@ constexpr auto gAndroidLayoutWebviewActivity = R"XML( constexpr auto gAndroidValuesStrings = R"XML( <resources> - <string name="app_name">{{build_name}}</string> + <string name="app_name">{{meta_title}}</string> </resources> )XML"; @@ -1637,11 +1827,10 @@ category = "" icon = "" ; TODO Signing guide: https://socketsupply.co/guides/#code-signing-certificates -sign = "" - codesign_identity = "" -sign_paths = "" +; Additional paths to codesign +codesign_paths = "" [native] From 21739de407a2b6cec44121f04ae403a78fcd1bf5 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:36:57 -0400 Subject: [PATCH 105/256] refactor(src/common.hh): support 'WAS_CODESIGNED' for apple --- src/common.hh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/common.hh b/src/common.hh index 48ef668f6b..ec249190f3 100644 --- a/src/common.hh +++ b/src/common.hh @@ -16,6 +16,10 @@ #include <objc/objc-runtime.h> #endif +#if !defined(WAS_CODESIGNED) +#define WAS_CODESIGNED 0 +#endif + #ifndef debug #if !defined(SSC_CLI) static os_log_t SSC_OS_LOG_DEBUG_BUNDLE = nullptr; From 5ec06ed75a806679748914c694f32367ddcd907f Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:37:24 -0400 Subject: [PATCH 106/256] refactor(bin/cflags.sh): remove 'libstdc++' for android --- bin/cflags.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/cflags.sh b/bin/cflags.sh index 319ce43a69..9dc0ea2034 100755 --- a/bin/cflags.sh +++ b/bin/cflags.sh @@ -41,7 +41,6 @@ if (( !TARGET_OS_ANDROID && !TARGET_ANDROID_EMULATOR )); then else source "$root/bin/android-functions.sh" android_fte > /dev/null - cflags+=("-stdlib=libstdc++") cflags+=("-DANDROID -pthreads -fexceptions -fPIC -frtti -fsigned-char -D_FILE_OFFSET_BITS=64 -Wno-invalid-command-line-argument -Wno-unused-command-line-argument") cflags+=("-I$(dirname $NDK_BUILD)/sources/cxx-stl/llvm-libc++/include") cflags+=("-I$(dirname $NDK_BUILD)/sources/cxx-stl/llvm-libc++abi/include") From 6d7201088e942e98bd2809f67c14fb2d0749d630 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:37:56 -0400 Subject: [PATCH 107/256] refactor(src/cli/cli.cc): upgrade android, improve codesign, support desktop in 'install-app' --- src/cli/cli.cc | 759 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 632 insertions(+), 127 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 94d2a711e1..4b9bb4bc77 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -72,6 +72,7 @@ Map rc; auto start = system_clock::now(); bool flagDebugMode = true; +bool flagVerboseMode = true; bool flagQuietMode = false; Map defaultTemplateAttrs; @@ -1166,7 +1167,7 @@ struct AndroidCliState { SSC::String androidHome; // android, android-emulator SSC::String targetPlatform; - // android-33 or current platform number + // android-34 or current platform number SSC::String platform; StringStream avdmanager; bool sscAvdExists = false; @@ -1744,6 +1745,7 @@ optionsAndEnv parseCommandLineOptions ( } if (equal(key, "--verbose")) { + flagVerboseMode = true; setEnv("SSC_VERBOSE", "1"); continue; } @@ -1765,6 +1767,9 @@ optionsAndEnv parseCommandLineOptions ( } else { // Option in the form "--key" or "-k" value = ""; + if (i + 1 < options.size() && options[i + 1][0] != '-') { + targetPath = fs::absolute(options[i + 1]).lexically_normal(); + } } } @@ -1782,8 +1787,20 @@ optionsAndEnv parseCommandLineOptions ( continue; } - auto option = validateOption(key, value, availableOptions, subcommand); - handleOption(optionsWithValue, optionsWithoutValue, option, value, subcommand); + if (key.size() && !key.starts_with("-")) { + targetPath = fs::absolute(key).lexically_normal(); + value = ""; + key = ""; + } + + if (key.size() > 0) { + auto option = validateOption(key, value, availableOptions, subcommand); + handleOption(optionsWithValue, optionsWithoutValue, option, value, subcommand); + } + } + + if (targetPath.empty()) { + targetPath = fs::current_path(); } result.optionsWithValue = optionsWithValue; @@ -1849,15 +1866,7 @@ int main (const int argc, const char* argv[]) { } auto const lastOption = argv[argc-1]; - int numberOfOptions = argc - 3; - - // if no path provided, use current directory - if (argc == 2 || lastOption[0] == '-') { - numberOfOptions = argc - 2; - targetPath = fs::current_path(); - } else { - targetPath = fs::absolute(lastOption).lexically_normal(); - } + const int numberOfOptions = argc - 2; #if defined(_WIN32) static String HOME = getEnv("HOMEPATH"); @@ -2122,10 +2131,8 @@ int main (const int argc, const char* argv[]) { // internal if (flagDebugMode) { - settings["apple_instruments"] = "true"; suffix += "-dev"; } else { - settings["apple_instruments"] = "false"; } settings["debug"] = flagDebugMode; @@ -2142,8 +2149,8 @@ int main (const int argc, const char* argv[]) { // first flag indicating whether option is optional // second flag indicating whether option should be followed by a value Options initOptions = { - { { "--config" }, true, false }, - { { "--name" }, true, true } + { { "--config", "-C" }, true, false }, + { { "--name", "-n" }, true, true } }; createSubcommand("init", initOptions, false, [&](Map optionsWithValue, std::unordered_set<String> optionsWithoutValue) -> void { auto isCurrentPathEmpty = fs::is_empty(fs::current_path()); @@ -2286,12 +2293,17 @@ int main (const int argc, const char* argv[]) { // first flag indicating whether option is optional // second flag indicating whether option should be followed by a value Options installAppOptions = { - { { "--platform" }, false, true }, - { { "--device" }, true, true } + { { "--debug", "-D" }, true, false }, + { { "--device" }, true, true }, + { { "--platform" }, true, true }, + { { "--prod", "-P" }, true, false }, + { { "--verbose", "-V" }, true, false }, + { { "--target" }, true, true } }; createSubcommand("install-app", installAppOptions, true, [&](Map optionsWithValue, std::unordered_set<String> optionsWithoutValue) -> void { String commandOptions = ""; String targetPlatform = optionsWithValue["--platform"]; + String installTarget = optionsWithValue["--target"]; if (targetPlatform.size() > 0) { // just assume android when 'android-emulator' is given @@ -2300,9 +2312,18 @@ int main (const int argc, const char* argv[]) { } if (targetPlatform != "ios" && targetPlatform != "android") { - std::cout << "Unsupported platform: " << targetPlatform << std::endl; + log("ERROR: Unsupported platform: " + targetPlatform); + exit(1); + } + } + + if (targetPlatform.size() == 0) { + if (!platform.mac && !platform.linux) { + log("ERROR: Unsupported host desktop platform. Only 'macOS' and 'Linux' is supported"); exit(1); } + + targetPlatform = platform.os; } auto device = optionsWithValue["--device"]; @@ -2331,15 +2352,20 @@ int main (const int argc, const char* argv[]) { // this command will install the app to the first connected device which was // added to the provisioning profile if no --device is provided. auto command = cfgUtilPath + " " + commandOptions + "install-app " + ipaPath.string(); + if (flagDebugMode) { + log(command); + } + auto r = exec(command); if (r.exitCode != 0) { log("ERROR: failed to install the app. Is the device plugged in?"); + if (flagDebugMode) { + log(r.output); + } exit(1); } - } - - if (targetPlatform == "android") { + } else if (targetPlatform == "android") { auto androidHome = getAndroidHome(); auto paths = getPaths(targetPlatform); auto output = paths.platformSpecificOutputPath; @@ -2367,9 +2393,114 @@ int main (const int argc, const char* argv[]) { log("ERROR: failed to install the app. Is the device plugged in?"); exit(1); } + } else if (platform.mac) { + if (installTarget.size() == 0) { + installTarget = "/"; + } + + auto paths = getPaths(targetPlatform); + auto pkgArchive = paths.platformSpecificOutputPath / (settings["build_name"] + ".pkg"); + auto zipArchive = paths.platformSpecificOutputPath / (settings["build_name"] + ".zip"); + auto appDirectory = paths.platformSpecificOutputPath / (settings["build_name"] + ".app"); + + if (fs::exists(pkgArchive)) { + String command = ""; + + if (installTarget == "/") { + command = "sudo installer"; + } else { + command = "installer"; + } + + if (flagVerboseMode) { + command += " -verbose"; + } + + command += " -pkg " + pkgArchive.string(); + command += " -target " + installTarget; + + auto r = exec(command); + if (r.exitCode != 0) { + log("ERROR: Failed to install app in target"); + + if (flagDebugMode) { + log(r.output); + } + exit(r.exitCode); + } + + if (flagVerboseMode) { + log(r.output); + } + } else if (fs::exists(zipArchive)) { + String command = ( + "ditto -x -k " + + zipArchive.string() + + " " + (Path(installTarget) / "Applications").string() + ); + + auto r = exec(command); + if (r.exitCode != 0) { + log("ERROR: Failed to unzip app to install target"); + if (flagDebugMode) { + log(r.output); + } + exit(r.exitCode); + } + + if (flagVerboseMode) { + log(r.output); + } + } else if (fs::exists(appDirectory)) { + String command = ( + "cp -r " + + appDirectory.string() + + " " + (Path(installTarget) / "Applications").string() + ); + + auto r = exec(command); + if (r.exitCode != 0) { + log("ERROR: Failed to copy app to install target"); + if (flagDebugMode) { + log(r.output); + } + exit(r.exitCode); + } + + if (flagVerboseMode) { + log(r.output); + } + } else { + log("ERROR: Unable to determine macOS application or package to install."); + exit(1); + } + } else if (platform.linux) { + auto paths = getPaths(targetPlatform); + auto debArchive = paths.pathPackage.string() + ".deb"; + + String command = "dpkg -i " + debArchive; + auto r = exec(command); + if (r.exitCode != 0) { + log("ERROR: Failed to install app to target"); + if (flagDebugMode) { + log(r.output); + } + exit(r.exitCode); + } + + if (flagVerboseMode) { + log(r.output); + } + } else { + log("ERROR: Unsupported platform target."); + exit(1); } - log("successfully installed the app on your device(s)."); + if (targetPlatform == "ios" || targetPlatform == "android") { + log("Successfully installed the app on your device(s)."); + } else { + log("Successfully installed the app to your desktop."); + } exit(0); }); @@ -2398,23 +2529,25 @@ int main (const int argc, const char* argv[]) { // second flag indicating whether option should be followed by a value Options runOptions = { { { "--platform" }, true, true }, - { { "--host" }, true, true }, - { { "--port" }, true, true }, - { { "--prod" }, true, false }, - { { "--test" }, true, true }, - { { "--headless" }, true, false } + { { "--prod", "-P" }, true, false }, + { { "--test", "-t" }, true, true }, + { { "--headless", "-H" }, true, false }, + { { "--debug", "-D" }, true, false }, + { { "--verbose", "-V" }, true, false }, }; Options buildOptions = { - { { "--quiet" }, true, false }, + { { "--quiet", "-q" }, true, false }, { { "--only-build", "-o" }, true, false }, { { "--run", "-r" }, true, false }, - { { "--watch", "-w" }, true, false }, - { { "-p" }, true, false }, - { { "-c" }, true, false }, - { { "-s" }, true, false }, - { { "-e" }, true, false }, - { { "-n" }, true, false } + { { "--watch", "-W" }, true, false }, + { { "--debug", "-D" }, true, false }, + { { "--verbose", "-V" }, true, false }, + { { "--prod", "-P" }, true, false }, + { { "--package", "-p" }, true, false }, + { { "--package-format", "-f" }, true, true }, + { { "--codesign", "-c" }, true, false }, + { { "--notarize", "-n" }, true, false } }; // Insert the elements of runOptions into buildOptions @@ -2442,14 +2575,12 @@ int main (const int argc, const char* argv[]) { String additionalBuildArgs = ""; bool flagRunUserBuildOnly = optionsWithoutValue.find("--only-build") != optionsWithoutValue.end() || equal(rc["build_only"], "true"); - bool flagAppStore = optionsWithoutValue.find("-s") != optionsWithoutValue.end() || equal(rc["build_app_store"], "true"); - bool flagCodeSign = optionsWithoutValue.find("-c") != optionsWithoutValue.end() || equal(rc["build_codesign"], "true"); + bool flagCodeSign = optionsWithoutValue.find("--codesign") != optionsWithoutValue.end() || equal(rc["build_codesign"], "true"); bool flagBuildHeadless = settings["build_headless"] == "true"; bool flagRunHeadless = optionsWithoutValue.find("--headless") != optionsWithoutValue.end(); bool flagShouldRun = optionsWithoutValue.find("--run") != optionsWithoutValue.end() || equal(rc["build_run"], "true"); - bool flagEntitlements = optionsWithoutValue.find("-e") != optionsWithoutValue.end() || equal(rc["build_entitlements"], "true"); - bool flagShouldNotarize = optionsWithoutValue.find("-n") != optionsWithoutValue.end() || equal(rc["build_notarize"], "true"); - bool flagShouldPackage = optionsWithoutValue.find("-p") != optionsWithoutValue.end() || equal(rc["build_package"], "true"); + bool flagShouldNotarize = optionsWithoutValue.find("--notarize") != optionsWithoutValue.end() || equal(rc["build_notarize"], "true"); + bool flagShouldPackage = optionsWithoutValue.find("--package") != optionsWithoutValue.end() || equal(rc["build_package"], "true"); bool flagBuildForIOS = false; bool flagBuildForAndroid = false; bool flagBuildForAndroidEmulator = false; @@ -2458,6 +2589,10 @@ int main (const int argc, const char* argv[]) { bool flagShouldWatch = optionsWithoutValue.find("--watch") != optionsWithoutValue.end() || equal(rc["build_watch"], "true"); String testFile = optionsWithValue["--test"]; + if (optionsWithValue["--package-format"].size() > 0) { + flagShouldPackage = true; + } + if (flagBuildTest && testFile.size() == 0) { log("ERROR: --test value is required."); exit(1); @@ -2475,11 +2610,6 @@ int main (const int argc, const char* argv[]) { flagQuietMode = true; } - if (flagEntitlements && !platform.mac) { - log("WARNING: Entitlements are only supported on macOS. Ignoring option."); - flagEntitlements = false; - } - if (flagShouldNotarize && !platform.mac) { log("WARNING: Notarization is only supported on macOS. Ignoring option."); flagShouldNotarize = false; @@ -2675,6 +2805,11 @@ int main (const int argc, const char* argv[]) { flags += " -framework Cocoa"; flags += " -framework OSLog"; flags += " -DMACOS=1"; + if (flagCodeSign) { + flags += " -DWAS_CODESIGNED=1"; + } else { + flags += " -DWAS_CODESIGNED=0"; + } flags += " -I" + prefixFile(); flags += " -I" + prefixFile("include"); flags += " -L" + prefixFile("lib/" + platform.arch + "-desktop"); @@ -2702,7 +2837,74 @@ int main (const int argc, const char* argv[]) { settings["mac_info_plist_data"] = ""; } - auto plistInfo = tmpl(gPListInfo, settings); + if (flagRunHeadless) { + settings["mac_info_plist_data"] += ( + " <key>LSBackgroundOnly</key>\n" + " <true/>\n" + ); + } + + // determine XCode version + do { + auto r = exec("xcodebuild -version | head -n1 | awk '{print $2}'"); + if (r.exitCode != 0) { + if (flagDebugMode) { + log("Failed to determine XCode version."); + log(r.output); + } + exit(r.exitCode); + } + + settings["__xcode_version"] = trim(r.output); + } while (0); + + // determine XCode Build version + do { + auto r = exec("xcodebuild -version | tail -n1 | awk '{print $3}'"); + if (r.exitCode != 0) { + log("Failed to determine XCode Build version code."); + if (flagDebugMode) { + log(r.output); + } + exit(r.exitCode); + } + + settings["__xcode_build_version"] = trim(r.output); + } while (0); + + // determine macOS SDK build version + do { + auto r = exec(" xcodebuild -sdk macosx -version | grep ProductBuildVersion | awk '{ print $2 }'"); + if (r.exitCode != 0) { + if (flagDebugMode) { + log("Failed to determine MacOSX SDK build version code."); + log(r.output); + } + exit(r.exitCode); + } + + settings["__xcode_macosx_sdk_build_version"] = trim(r.output); + } while (0); + + // determine macOS SDK version + do { + auto r = exec(" xcodebuild -sdk macosx -version | grep ProductVersion | awk '{ print $2 }'"); + if (r.exitCode != 0) { + if (flagDebugMode) { + log("Failed to determine MacOSX SDK version."); + log(r.output); + } + exit(r.exitCode); + } + + settings["__xcode_macosx_sdk_version"] = trim(r.output); + } while (0); + + if (settings["mac_minimum_supported_version"].size() == 0) { + settings["mac_minimum_supported_version"] = "13.0.0"; + } + + auto plistInfo = tmpl(gMacOSInfoPList, settings); writeFile(paths.pathPackage / pathBase / "Info.plist", plistInfo); @@ -2978,8 +3180,7 @@ int main (const int argc, const char* argv[]) { } if (settings["android_allow_cleartext"].size() == 0) { - if (flagDebugMode) - { + if (flagDebugMode) { settings["android_allow_cleartext"] = "android:usesCleartextTraffic=\"true\"\n"; } else { settings["android_allow_cleartext"] = ""; @@ -3558,14 +3759,14 @@ int main (const int argc, const char* argv[]) { settings["ios_provisioning_specifier"] = provSpec; settings["ios_provisioning_profile"] = uuid; - settings["apple_team_id"] = team; + settings["apple_team_identifier"] = team; } if (flagBuildForSimulator) { settings["ios_provisioning_specifier"] = ""; settings["ios_provisioning_profile"] = ""; settings["ios_codesign_identity"] = ""; - settings["apple_team_id"] = ""; + settings["apple_team_identifier"] = ""; } // --platform=ios should always build for arm64 even on Darwin x86_64 @@ -3615,6 +3816,7 @@ int main (const int argc, const char* argv[]) { {"SSC_SETTINGS", _settings}, {"SSC_VERSION", VERSION_STRING}, {"SSC_VERSION_HASH", VERSION_HASH_STRING}, + {"WAS_CODESIGNED", flagCodeSign ? "1" : "0"}, {"__ios_native_extensions_build_ids", ""}, {"__ios_native_extensions_build_refs", ""}, {"__ios_native_extensions_build_context_refs", ""}, @@ -3974,7 +4176,7 @@ int main (const int argc, const char* argv[]) { } writeFile(paths.platformSpecificOutputPath / "exportOptions.plist", tmpl(gXCodeExportOptions, settings)); - writeFile(paths.platformSpecificOutputPath / "Info.plist", tmpl(gXCodePlist, settings)); + writeFile(paths.platformSpecificOutputPath / "Info.plist", tmpl(gIOSInfoPList, settings)); writeFile(pathToProject / "project.pbxproj", tmpl(gXCodeProject, xCodeProjectVariables)); writeFile(pathToScheme / schemeName, tmpl(gXCodeScheme, settings)); @@ -4219,8 +4421,75 @@ int main (const int argc, const char* argv[]) { fs::create_directories(pathBase); writeFile(pathBase / "LaunchScreen.storyboard", gStoryboardLaunchScreen); - // TODO allow the user to copy their own if they have one - writeFile(pathToDist / "socket.entitlements", tmpl(gXcodeEntitlements, settings)); + + Map entitlementSettings; + extendMap(entitlementSettings, settings); + + if (flagDebugMode) { + entitlementSettings["configured_entitlements"] += ( + " <key>get-task-allow</key>\n" + " <true/>\n " + ); + + entitlementSettings["configured_entitlements"] += ( + " <key>com.apple.security.cs.debugger</key>\n" + " <true/>\n " + ); + } + + if (settings["permission_allow_user_media"] != "false") { + if (settings["permission_allow_camera"] != "false") { + entitlementSettings["configured_entitlements"] += ( + " <key>com.apple.security.device.camera</key>\n" + " <true/>\n" + ); + } + + if (settings["permission_allow_microphone"] != "false") { + entitlementSettings["configured_entitlements"] += ( + " <key>com.apple.security.device.microphone</key>\n" + " <true/>\n" + ); + } + } + + if (settings["permissions_allow_bluetooth"] != "false") { + entitlementSettings["configured_entitlements"] += ( + " <key>com.apple.security.device.bluetooth</key>\n" + " <true/>\n" + ); + } + + if (settings["permissions_allow_notifications"] != "false") { + entitlementSettings["configured_entitlements"] += ( + " <key>com.apple.developer.usernotifications.filtering</key>\n" + " <true/>\n" + ); + } + + if (settings["permissions_allow_geolocation"] != "false") { + entitlementSettings["configured_entitlements"] += ( + " <key>com.apple.security.personal-information.location</key>\n" + " <true/>\n" + ); + + entitlementSettings["configured_entitlements"] += ( + " <key>com.apple.developer.location.push</key>\n" + " <true/>\n" + ); + } + + if (settings["ios_sandbox"] != "false") { + entitlementSettings["configured_entitlements"] += ( + " <key>com.apple.security.app-sandbox</key>\n" + " <true/>\n" + ); + } + + writeFile( + pathToDist / "socket.entitlements", + tmpl(gXcodeEntitlements, entitlementSettings) + ); // // For iOS we're going to bail early and let XCode infrastructure handle @@ -4318,8 +4587,8 @@ int main (const int argc, const char* argv[]) { StringStream sdkmanager; StringStream packages; StringStream gradlew; - String ndkVersion = "25.0.8775105"; - String androidPlatform = "android-33"; + String ndkVersion = "26.0.10792818"; + String androidPlatform = "android-34"; if (platform.unix) { gradlew @@ -5004,7 +5273,7 @@ int main (const int argc, const char* argv[]) { if (platform.mac) { if (isForDesktop) { - settings["mac_sign_paths"] += (paths.pathResourcesRelativeToUserBuild / "socket" / "extensions" / extension / (extension + SHARED_OBJ_EXT)).string() + ";"; + settings["mac_codesign_paths"] += (paths.pathResourcesRelativeToUserBuild / "socket" / "extensions" / extension / (extension + SHARED_OBJ_EXT)).string() + ";"; } } @@ -5077,38 +5346,81 @@ int main (const int argc, const char* argv[]) { // --- // if (flagShouldPackage && platform.linux && isForDesktop) { - Path pathSymLinks = { - paths.pathPackage / - "usr" / - "local" / - "bin" - }; + auto packageFormat = settings["build_linux_package_format"]; - auto linuxExecPath = Path { - Path("/opt") / - settings["build_name"] / - settings["build_name"] - }; + if (optionsWithValue["--package-format"].size()) { + packageFormat = optionsWithValue["--package-format"]; + } - fs::create_directories(pathSymLinks); - fs::create_symlink( - linuxExecPath, - pathSymLinks / settings["build_name"] - ); + if (packageFormat.size() == 0) { + packageFormat = "deb"; + } - StringStream archiveCommand; + if (packageFormat == "deb") { + Path pathSymLinks = { + paths.pathPackage / + "usr" / + "local" / + "bin" + }; - archiveCommand - << "dpkg-deb --build --root-owner-group " - << paths.pathPackage.string() - << " " - << (paths.platformSpecificOutputPath).string(); + auto linuxExecPath = Path { + Path("/opt") / + settings["build_name"] / + settings["build_name"] + }; + + fs::create_directories(pathSymLinks); + fs::create_symlink( + linuxExecPath, + pathSymLinks / settings["build_name"] + ); - if (debugEnv || verboseEnv) log(archiveCommand.str()); - auto r = std::system(archiveCommand.str().c_str()); + StringStream archiveCommand; - if (r != 0) { - log("ERROR: failed to create deb package"); + archiveCommand + << "dpkg-deb --build --root-owner-group " + << paths.pathPackage.string() + << " " + << (paths.platformSpecificOutputPath).string(); + + if (debugEnv || verboseEnv) { + log(archiveCommand.str()); + } + + auto r = exec(archiveCommand.str()); + + if (r.exitCode != 0) { + log("ERROR: Build packaging failed for Linux"); + if (flagDebugMode) { + log(r.output); + } + exit(r.exitCode); + } + } else if (packageFormat == "zip") { + StringStream zipCommand; + + zipCommand + << "zip -r" + << " " << (paths.platformSpecificOutputPath / (settings["build_name"] + ".zip")).string() + << " " << pathResourcesRelativeToUserBuild.string() + ; + + if (debugEnv || verboseEnv) { + log(zipCommand.str()); + } + + auto r = exec(zipCommand.str()); + + if (r.exitCode != 0) { + log("ERROR: Build packaging failed for Linux"); + if (flagDebugMode) { + log(r.output); + } + exit(r.exitCode); + } + } else { + log("ERROR: Unknown package format given in '[build.linux.package] format = \"" + packageFormat + "\""); exit(1); } } @@ -5141,15 +5453,72 @@ int main (const int argc, const char* argv[]) { // https://developer.apple.com/forums/thread/128166 // https://wiki.lazarus.freepascal.org/Code_Signing_for_macOS // + Path pathBase = "Contents"; StringStream signCommand; String entitlements = ""; - if (settings.count("entitlements") == 1) { - // entitlements = " --entitlements " + (targetPath / settings["entitlements"]).string(); + Map entitlementSettings; + extendMap(entitlementSettings, settings); + + if (settings["permission_allow_user_media"] != "false") { + if (settings["permission_allow_camera"] != "false") { + entitlementSettings["configured_entitlements"] += ( + " <key>com.apple.security.device.camera</key>\n" + " <true/>\n" + ); + } + + if (settings["permission_allow_microphone"] != "false") { + entitlementSettings["configured_entitlements"] += ( + " <key>com.apple.security.device.microphone</key>\n" + " <true/>\n" + " <key>com.apple.security.device.audio-input</key>\n" + " <true/>\n" + ); + } + } + + if (settings["permissions_allow_bluetooth"] != "false") { + entitlementSettings["configured_entitlements"] += ( + " <key>com.apple.security.device.bluetooth</key>\n" + " <true/>\n" + ); + } + + if (settings["permissions_allow_geolocation"] != "false") { + entitlementSettings["configured_entitlements"] += ( + " <key>com.apple.security.personal-information.location</key>\n" + " <true/>\n" + ); + } + + if (settings["apple_team_identifier"].size() > 0) { + auto identifier = ( + settings["apple_team_identifier"] + + "." + + settings["meta_bundle_identifier"] + ); + + entitlementSettings["configured_entitlements"] += ( + " <key>com.apple.application-identifier</key>\n" + " <string>" + identifier + "</string>\n" + ); + } + + if (settings["mac_sandbox"] != "false") { + entitlementSettings["configured_entitlements"] += ( + " <key>com.apple.security.app-sandbox</key>\n" + " <true/>\n" + ); } - if (settings.count("mac_sign") == 0) { - log("'mac_sign' key/value is required"); + writeFile( + paths.platformSpecificOutputPath / "socket.entitlements", + tmpl(gXcodeEntitlements, entitlementSettings) + ); + + if (settings.count("mac_codesign_identity") == 0) { + log("'[mac.codesign] identity' key/value is required"); exit(1); } @@ -5159,13 +5528,14 @@ int main (const int argc, const char* argv[]) { << " --force" << " --options runtime" << " --timestamp" - << entitlements - << " --sign '" + settings["mac_sign"] + "'" + << " --entitlements " << (paths.platformSpecificOutputPath / "socket.entitlements").string() + << " --identifier '" << settings["meta_bundle_identifier"] << "'" + << " --sign '" << settings["mac_codesign_identity"] << "'" << " " ; - if (settings["mac_sign_paths"].size() > 0) { - auto paths = split(settings["mac_sign_paths"], ';'); + if (settings["mac_codesign_paths"].size() > 0) { + auto paths = split(settings["mac_codesign_paths"], ';'); for (int i = 0; i < paths.size(); i++) { String prefix = (i > 0) ? ";" : ""; @@ -5177,7 +5547,7 @@ int main (const int argc, const char* argv[]) { << (pathResources / paths[i]).string() ; } - signCommand << ";"; + signCommand << "&& "; } signCommand @@ -5185,24 +5555,49 @@ int main (const int argc, const char* argv[]) { << commonFlags.str() << binaryPath.string() - << "; codesign" + << " && codesign" << commonFlags.str() << paths.pathPackage.string(); - log(signCommand.str()); + if (flagDebugMode) { + log(signCommand.str()); + } + auto r = exec(signCommand.str()); if (r.output.size() > 0) { if (r.exitCode != 0) { - log("Unable to sign"); - std::cerr << r.output << std::endl; + log("ERROR: Unable to sign application with 'codesign'"); + if (flagDebugMode) { + log(r.output); + } exit(r.exitCode); - } else { - std::cout << r.output << std::endl; } } - log("finished code signing"); + if (flagVerboseMode) { + log(r.output); + } + + log("Successfully code signed app with 'codesign'"); + } + + if (platform.mac && isForDesktop) { + auto packageFormat = settings["build_mac_package_format"]; + + if (optionsWithValue["--package-format"].size()) { + packageFormat = optionsWithValue["--package-format"]; + } + + if (packageFormat.size() == 0) { + packageFormat = "zip"; + settings["build_mac_package_format"] = packageFormat; + } + + pathToArchive = paths.platformSpecificOutputPath / (settings["build_name"] + "." + packageFormat); + if (!flagShouldPackage && flagShouldNotarize && !fs::exists(pathToArchive)) { + flagShouldPackage = true; + } } // @@ -5210,27 +5605,91 @@ int main (const int argc, const char* argv[]) { // --- // if (flagShouldPackage && platform.mac && isForDesktop) { - StringStream zipCommand; - auto ext = ".zip"; - auto pathToBuild = paths.platformSpecificOutputPath / "build"; - - pathToArchive = paths.platformSpecificOutputPath / (settings["build_name"] + ext); - - zipCommand - << "ditto" - << " -c" - << " -k" - << " --sequesterRsrc" - << " --keepParent" - << " " - << paths.pathPackage.string() - << " " - << pathToArchive.string(); + auto packageFormat = settings["build_mac_package_format"]; + + if (optionsWithValue["--package-format"].size()) { + packageFormat = optionsWithValue["--package-format"]; + } + + if (packageFormat == "zip") { + StringStream zipCommand; + zipCommand + << "ditto" + << " -c" + << " -k" + << " --sequesterRsrc" + << " --keepParent" + << " " + << paths.pathPackage.string() + << " " + << pathToArchive.string(); + + auto r = exec(zipCommand.str()); - auto r = std::system(zipCommand.str().c_str()); + if (r.exitCode != 0) { + log("ERROR: Build packaging fails for macOS"); + if (flagDebugMode) { + log(r.output); + } + exit(r.exitCode); + } + } else if (packageFormat == "pkg") { + StringStream productBuildCommand; + auto identity = settings["mac_productbuild_identity"]; + auto productBuildInstallPath = settings["mac_productbuild_install_path"]; - if (r != 0) { - log("ERROR: failed to create zip for notarization"); + if (identity.size() == 0) { + log("ERROR: Missing '[mac.productbuild] identity = ...' in 'socket.ini'"); + exit(1); + } + + if (productBuildInstallPath.size() == 0) { + productBuildInstallPath = "/Applications"; + } + + auto productBuildOutput = (paths.platformSpecificOutputPath / Path(settings["build_name"] + ".pkg")).string(); + productBuildCommand + << "xcrun productbuild" + << " --sign '" << identity << "'" + << " --version '" << settings["meta_version"] << "'" + << " --component '" << paths.pathPackage.string() << "'" << " " << productBuildInstallPath + << " --identifier '" << settings["meta_bundle_identifier"] << "'" + << " --timestamp" + << " " << productBuildOutput; + + if (verboseEnv) { + log(productBuildCommand.str()); + } + + auto r = exec(productBuildCommand.str()); + + if (r.exitCode != 0) { + log("ERROR: Failed to package macOS application with 'productbuild'"); + if (flagDebugMode) { + log(r.output); + } + exit(r.exitCode); + } + + if (verboseEnv) { + log(r.output); + } + + r = exec(String("xcrun pkgutil --check-signature ") + productBuildOutput); + + if (r.exitCode != 0) { + log("ERROR: Failed to verify macOS package signature with 'pkgutil'"); + if (flagDebugMode) { + log(r.output); + } + exit(r.exitCode); + } + + if (verboseEnv) { + log(r.output); + } + } else { + log("ERROR: Unknown package format given in '[build.mac.package] format = \"" + packageFormat + "\""); exit(1); } @@ -5246,20 +5705,59 @@ int main (const int argc, const char* argv[]) { String username = getEnv("APPLE_ID"); String password = getEnv("APPLE_ID_PASSWORD"); + if (username.size() == 0) { + username = settings["apple_identifier"]; + } + + if (username.size() == 0) { + log( + "ERROR: AppleID identifier could not be determined. " + "Please set '[apple] identifier = ...' or 'APPLE_ID' environment variable." + ); + exit(1); + } + + if (password.size() == 0) { + log( + "ERROR: AppleID identifier could not be determined. " + "Please set the 'APPLE_ID_PASSWORD' environment variable." + ); + exit(1); + } + + if (!fs::exists(pathToArchive)) { + log( + "ERROR: Cannot notarize application: Package archive does not exists." + ); + exit(1); + } + notarizeCommand << "xcrun" - << " altool" - << " --notarize-app" - << " --username \"" << username << "\"" - << " --password \"" << password << "\"" - << " --primary-bundle-id \"" << settings["meta_bundle_identifier"] << "\"" - << " --file \"" << pathToArchive.string() << "\"" - ; + << " notarytool submit" + << " --wait" + << " --apple-id \"" << username << "\"" + << " --password \"" << password << "\""; + + if (settings["apple_team_identifier"].size() > 0) { + notarizeCommand << " --team-id " << settings["apple_team_identifier"]; + } + + notarizeCommand << " \"" << pathToArchive.string() << "\""; + + if (flagDebugMode) { + log(notarizeCommand.str()); + } auto r = exec(notarizeCommand.str().c_str()); if (r.exitCode != 0) { - log("Unable to notarize"); + log("Unable to notarize macOS application"); + + if (flagDebugMode) { + log(r.output); + } + exit(r.exitCode); } @@ -5741,7 +6239,7 @@ int main (const int argc, const char* argv[]) { String quote = !platform.win ? "'" : "\""; String slash = !platform.win ? "/" : "\\"; - auto androidPlatform = "android-33"; + auto androidPlatform = "android-34"; AndroidCliState androidState; androidState.androidHome = getAndroidHome(); androidState.verbose = debugEnv || verboseEnv; @@ -5759,7 +6257,10 @@ int main (const int argc, const char* argv[]) { // second flag indicating whether option should be followed by a value Options setupOptions = { { { "--platform" }, true, true }, - { { "--yes", "-y" }, true, false } + { { "--yes", "-y" }, true, false }, + { { "--quiet", "-q" }, true, false }, + { { "--debug", "-D" }, true, false }, + { { "--verbose", "-V" }, true, false }, }; createSubcommand("setup", setupOptions, false, [&](Map optionsWithValue, std::unordered_set<String> optionsWithoutValue) -> void { auto help = false; @@ -5845,6 +6346,10 @@ int main (const int argc, const char* argv[]) { } } + if (optionsWithoutValue.find("--quiet") != optionsWithoutValue.end() || equal(rc["build_quiet"], "true")) { + flagQuietMode = true; + } + log("Running setup for platform '" + targetPlatform + "' in " + "SOCKET_HOME (" + prefixFile() + ")"); String command = scriptHost + " \"" + script.string() + "\" " + argument + " " + yesArg; auto r = std::system(command.c_str()); From 25be11f6dd57cbd3e62686c8b2a23f0f8e36fbb6 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:38:07 -0400 Subject: [PATCH 108/256] chore(bin/android-functions.sh): upgrade android --- bin/android-functions.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/android-functions.sh b/bin/android-functions.sh index 0c4977bf8d..47a1610267 100755 --- a/bin/android-functions.sh +++ b/bin/android-functions.sh @@ -487,8 +487,8 @@ function android_supported_abis() { } export ANDROID_DEPS_ERROR -declare ANDROID_PLATFORM="33" -declare NDK_VERSION="25.0.8775105" +declare ANDROID_PLATFORM="34" +declare NDK_VERSION="26.0.10792818" export BUILD_ANDROID From 5dc804940b0735e98c3bd482d5457e2b3a199d48 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:38:24 -0400 Subject: [PATCH 109/256] chore(bin/generate-gradle-files.sh): upgrade android/deps --- bin/generate-gradle-files.sh | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/bin/generate-gradle-files.sh b/bin/generate-gradle-files.sh index f069d97824..b1e9e5a2f8 100755 --- a/bin/generate-gradle-files.sh +++ b/bin/generate-gradle-files.sh @@ -10,7 +10,7 @@ rm -f "$root/build.gradle" "$root/gradle.properties" ## build.gradle cat > "$root/build.gradle" << GRADLE buildscript { - ext.kotlin_version = '1.7.0' + ext.kotlin_version = '1.9.10' repositories { google() mavenCentral() @@ -33,25 +33,26 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' android { - compileSdkVersion 33 - ndkVersion "25.0.8775105" + compileSdkVersion 34 + ndkVersion "26.0.10792818" flavorDimensions "default" defaultConfig { applicationId "__BUNDLE_IDENTIFIER__" minSdkVersion 24 - targetSdkVersion 33 + targetSdkVersion 34 versionCode 1 versionName "0.0.1" } } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:\$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' - implementation 'androidx.appcompat:appcompat:1.5.0' - implementation 'androidx.webkit:webkit:1.4.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.73' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.core:core-ktx:2.2.0' + implementation 'androidx.webkit:webkit:1.8.0' } GRADLE From 9151dd1a95aa88bef659eabdcd6cefca7531dfcc Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:38:36 -0400 Subject: [PATCH 110/256] refactor(src/window/apple.mm): clean up --- src/window/apple.mm | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/window/apple.mm b/src/window/apple.mm index c529f9ca0d..67630b9600 100644 --- a/src/window/apple.mm +++ b/src/window/apple.mm @@ -616,7 +616,7 @@ - (void) webView: (WKWebView*) webView dispatch_async(dispatch_get_main_queue(), ^{ this->eval(js); }); }; - this->bridge->router.map("window.eval", [=](auto message, auto router, auto reply) { + this->bridge->router.map("window.eval", [=, this](auto message, auto router, auto reply) { auto value = message.value; auto seq = message.seq; auto script = [NSString stringWithUTF8String: value.c_str()]; @@ -823,12 +823,6 @@ - (void) webView: (WKWebView*) webView forKey: @"drawsTransparentBackground" ]; */ - /* [[NSNotificationCenter defaultCenter] addObserver: webview - selector: @selector(systemColorsDidChangeNotification:) - name: NSSystemColorsDidChangeNotification - object: nil - ]; */ - // [webview registerForDraggedTypes: // [NSArray arrayWithObject:NSPasteboardTypeFileURL]]; // @@ -871,7 +865,7 @@ - (void) webView: (WKWebView*) webView [SSCWindowDelegate class], @selector(userContentController:didReceiveScriptMessage:), imp_implementationWithBlock( - [=](id self, SEL cmd, WKScriptMessage* scriptMessage) { + [=, this](id self, SEL cmd, WKScriptMessage* scriptMessage) { auto window = (Window*) objc_getAssociatedObject(self, "window"); if (!scriptMessage || !window) return; From b6545ca99adead8dfcc7f7052ee8d1141d8f01ea Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:38:54 -0400 Subject: [PATCH 111/256] fix(include/socket/platform.h): set android to '0' on apple --- include/socket/platform.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/socket/platform.h b/include/socket/platform.h index 38de24b7f5..e01d1f9e3b 100644 --- a/include/socket/platform.h +++ b/include/socket/platform.h @@ -30,7 +30,7 @@ #elif defined(__APPLE__) # include <TargetConditionals.h> # define SOCKET_RUNTIME_PLATFORM_NAME "darwin" -# define SOCKET_RUNTIME_PLATFORM_ANDROID 1 +# define SOCKET_RUNTIME_PLATFORM_ANDROID 0 # define SOCKET_RUNTIME_PLATFORM_IOS_SIMULATOR 0 # define SOCKET_RUNTIME_PLATFORM_LINUX 0 # define SOCKET_RUNTIME_PLATFORM_WINDOWS 0 From 54119780fb3f6e58ed26417d2a79657cb7260539 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:39:20 -0400 Subject: [PATCH 112/256] refactor(include/socket/extension.h): include macro'd type prototypes --- include/socket/extension.h | 46 +++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/include/socket/extension.h b/include/socket/extension.h index 5b86c876cd..8720f8fb35 100644 --- a/include/socket/extension.h +++ b/include/socket/extension.h @@ -92,6 +92,51 @@ #if defined(__cplusplus) extern "C" { #endif + typedef struct sapi_extension_registration sapi_extension_registration_t; + + /** + * Internal APIs + */ + + /** + * Internal ABI version getter for a registered extension. + * @ignore + * @private + */ + SOCKET_RUNTIME_EXTENSION_EXPORT + unsigned long __sapi_extension_abi (); + + /** + * Internal name getter for a registered extension. + * @ignore + * @private + */ + SOCKET_RUNTIME_EXTENSION_EXPORT + const char* __sapi_extension_name (); + + /** + * Internal description getter for a registered extension. + * @ignore + * @private + */ + SOCKET_RUNTIME_EXTENSION_EXPORT + const char* __sapi_extension_description (); + + /** + * Internal version getter for a registered extension. + * @ignore + * @private + */ + SOCKET_RUNTIME_EXTENSION_EXPORT + const char* __sapi_extension_version (); + + /** + * Internal initializer for a registered extension. + * @ignore + * @private + */ + SOCKET_RUNTIME_EXTENSION_EXPORT + const sapi_extension_registration_t* __sapi_extension_init (); /** * Context API @@ -692,7 +737,6 @@ extern "C" { /** * A container for an extension registration. */ - typedef struct sapi_extension_registration sapi_extension_registration_t; struct sapi_extension_registration { unsigned long abi; // required From 4e96734fe556f01eda7ad8580795955c488d2ba6 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:40:26 -0400 Subject: [PATCH 113/256] refactor(src/ipc/bridge.cc): improve geolocation/notification permission/api life cycles for apple --- src/ipc/bridge.cc | 668 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 645 insertions(+), 23 deletions(-) diff --git a/src/ipc/bridge.cc b/src/ipc/bridge.cc index 4b2cf4012c..baff8722c3 100644 --- a/src/ipc/bridge.cc +++ b/src/ipc/bridge.cc @@ -16,6 +16,9 @@ using namespace SSC; using namespace SSC::IPC; #if defined(__APPLE__) +static std::map<String, Router*> notificationRouterMap; +static Mutex notificationRouterMapMutex; + static dispatch_queue_attr_t qos = dispatch_queue_attr_make_with_qos_class( DISPATCH_QUEUE_CONCURRENT, QOS_CLASS_USER_INITIATED, @@ -1193,6 +1196,277 @@ static void initRouterTable (Router *router) { #endif }); +#if defined(__APPLE__) + router->map("notification.show", [](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, { + "id", + "title" + }); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + auto attachments = [NSMutableArray new]; + auto userInfo = [NSMutableDictionary new]; + auto content = [UNMutableNotificationContent new]; + auto __block id = message.get("id"); + + if (message.has("tag")) { + userInfo[@"tag"] = @(message.get("tag").c_str()); + content.threadIdentifier = @(message.get("tag").c_str()); + } + + if (message.has("lang")) { + userInfo[@"lang"] = @(message.get("lang").c_str()); + } + + if (!message.has("silent") && message.get("silent") == "false") { + content.sound = [UNNotificationSound defaultSound]; + } + + if (message.has("icon")) { + NSError* error = nullptr; + auto url = [NSURL URLWithString: @(message.get("icon").c_str())]; + + if (message.get("icon").starts_with("socket://")) { + url = [NSURL URLWithString: [NSBundle.mainBundle.resourcePath + stringByAppendingPathComponent: [NSString + #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + stringWithFormat: @"/ui/%s", url.path.UTF8String + #else + stringWithFormat: @"/%s", url.path.UTF8String + #endif + ] + ]]; + + url = [NSURL fileURLWithPath: url.path]; + } + + auto types = [UTType + typesWithTag: url.pathExtension + tagClass: UTTagClassFilenameExtension + conformingToType: nullptr + ]; + + auto options = [NSMutableDictionary new]; + + if (types.count > 0) { + options[UNNotificationAttachmentOptionsTypeHintKey] = types.firstObject.preferredMIMEType; + }; + + auto attachment = [UNNotificationAttachment + attachmentWithIdentifier: @("") + URL: url + options: options + error: &error + ]; + + if (error != nullptr) { + auto message = String( + error.localizedFailureReason.UTF8String != nullptr + ? error.localizedFailureReason.UTF8String + : "An unknown error occurred" + ); + + auto err = JSON::Object::Entries { { "message", message } }; + return reply(Result::Err { message, err }); + } + + [attachments addObject: attachment]; + } else { + // using an asset from the resources directory will require a code signed application + #if WAS_CODESIGNED + NSError* error = nullptr; + auto url = [NSURL URLWithString: [NSBundle.mainBundle.resourcePath + stringByAppendingPathComponent: [NSString + #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + stringWithFormat: @"/ui/icon.png" + #else + stringWithFormat: @"/icon.png" + #endif + ] + ]]; + + url = [NSURL fileURLWithPath: url.path]; + + auto types = [UTType + typesWithTag: url.pathExtension + tagClass: UTTagClassFilenameExtension + conformingToType: nullptr + ]; + + auto options = [NSMutableDictionary new]; + + auto attachment = [UNNotificationAttachment + attachmentWithIdentifier: @("") + URL: url + options: options + error: &error + ]; + + if (error != nullptr) { + auto message = String( + error.localizedFailureReason.UTF8String != nullptr + ? error.localizedFailureReason.UTF8String + : "An unknown error occurred" + ); + + auto err = JSON::Object::Entries { { "message", message } }; + + return reply(Result::Err { message, err }); + } + + [attachments addObject: attachment]; + #endif + } + + if (message.has("image")) { + NSError* error = nullptr; + auto url = [NSURL URLWithString: @(message.get("image").c_str())]; + + if (message.get("image").starts_with("socket://")) { + url = [NSURL URLWithString: [NSBundle.mainBundle.resourcePath + stringByAppendingPathComponent: [NSString + #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + stringWithFormat: @"/ui/%s", url.path.UTF8String + #else + stringWithFormat: @"/%s", url.path.UTF8String + #endif + ] + ]]; + + url = [NSURL fileURLWithPath: url.path]; + } + + auto types = [UTType + typesWithTag: url.pathExtension + tagClass: UTTagClassFilenameExtension + conformingToType: nullptr + ]; + + auto options = [NSMutableDictionary new]; + + if (types.count > 0) { + options[UNNotificationAttachmentOptionsTypeHintKey] = types.firstObject.preferredMIMEType; + }; + + auto attachment = [UNNotificationAttachment + attachmentWithIdentifier: @("") + URL: url + options: options + error: &error + ]; + + if (error != nullptr) { + auto err = JSON::Object::Entries { + { "message", String(error.localizedFailureReason.UTF8String) } + }; + + return reply(Result::Err { message, err }); + } + + [attachments addObject: attachment]; + } + + content.attachments = attachments; + content.userInfo = userInfo; + content.title = @(message.get("title").c_str()); + content.body = @(message.get("body", "").c_str()); + + auto request = [UNNotificationRequest + requestWithIdentifier: @(id.c_str()) + content: content + trigger: nil + ]; + + { + Lock lock(notificationRouterMapMutex); + notificationRouterMap.insert_or_assign(id, router); + } + + [notificationCenter addNotificationRequest: request withCompletionHandler: ^(NSError* error) { + if (error != nullptr) { + auto message = String( + error.localizedFailureReason.UTF8String != nullptr + ? error.localizedFailureReason.UTF8String + : "An unknown error occurred" + ); + + auto err = JSON::Object::Entries { + { "message", message } + }; + + reply(Result::Err { message, err }); + Lock lock(notificationRouterMapMutex); + notificationRouterMap.erase(id); + return; + } + + reply(Result { message.seq, message, JSON::Object::Entries { + {"id", request.identifier.UTF8String} + }}); + }]; + }); + + router->map("notification.close", [](auto message, auto router, auto reply) { + auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + auto err = validateMessageParameters(message, { "id" }); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + auto id = message.get("id"); + auto identifiers = @[@(id.c_str())]; + + [notificationCenter removePendingNotificationRequestsWithIdentifiers: identifiers]; + [notificationCenter removeDeliveredNotificationsWithIdentifiers: identifiers]; + + reply(Result { message.seq, message, JSON::Object::Entries { + {"id", id} + }}); + + Lock lock(notificationRouterMapMutex); + if (notificationRouterMap.contains(id)) { + auto notificationRouter = notificationRouterMap.at(id); + JSON::Object json = JSON::Object::Entries { + {"id", id}, + {"action", "dismiss"} + }; + + notificationRouter->emit("notificationresponse", json.str()); + notificationRouterMap.erase(id); + } + }); + + router->map("notification.list", [](auto message, auto router, auto reply) { + auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + [notificationCenter getDeliveredNotificationsWithCompletionHandler: ^(NSArray<UNNotification*> *notifications) { + JSON::Array::Entries entries; + + Lock lock(notificationRouterMapMutex); + for (UNNotification* notification in notifications) { + auto id = String(notification.request.identifier.UTF8String); + + if ( + !notificationRouterMap.contains(id) || + notificationRouterMap.at(id) != router + ) { + continue; + } + + entries.push_back(JSON::Object::Entries { + {"id", id} + }); + } + + reply(Result { message.seq, message, entries }); + }]; + }); +#endif + /** * Read or modify the `SEND_BUFFER` or `RECV_BUFFER` for a peer socket. * @param id Handle ID for the buffer to read/modify @@ -1256,6 +1530,170 @@ static void initRouterTable (Router *router) { router->core->os.availableMemory(message.seq, RESULT_CALLBACK_FROM_CORE_CALLBACK(message, reply)); }); + router->map("permissions.query", [](auto message, auto router, auto reply) { + auto err = validateMessageParameters(message, {"name"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + auto name = message.get("name"); + + #if defined(__APPLE__) + if (name == "geolocation") { + if (router->locationObserver.isAuthorized) { + auto data = JSON::Object::Entries {{"state", "granted"}}; + return reply(Result::Data { message, data }); + } else if (router->locationObserver.locationManager) { + auto authorizationStatus = ( + router->locationObserver.locationManager.authorizationStatus + ); + + if (authorizationStatus == kCLAuthorizationStatusDenied) { + auto data = JSON::Object::Entries {{"state", "denied"}}; + return reply(Result::Data { message, data }); + } else { + auto data = JSON::Object::Entries {{"state", "prompt"}}; + return reply(Result::Data { message, data }); + } + } + + auto data = JSON::Object::Entries {{"state", "denied"}}; + return reply(Result::Data { message, data }); + } + + if (name == "notifications") { + auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + [notificationCenter getNotificationSettingsWithCompletionHandler: ^(UNNotificationSettings *settings) { + if (!settings) { + auto err = JSON::Object::Entries {{ "message", "Failed to reach user notification settings" }}; + return reply(Result::Err { message, err }); + } + + if (settings.authorizationStatus == UNAuthorizationStatusDenied) { + auto data = JSON::Object::Entries {{"state", "denied"}}; + return reply(Result::Data { message, data }); + } else if (settings.authorizationStatus == UNAuthorizationStatusNotDetermined) { + auto data = JSON::Object::Entries {{"state", "prompt"}}; + return reply(Result::Data { message, data }); + } + + auto data = JSON::Object::Entries {{"state", "granted"}}; + return reply(Result::Data { message, data }); + }]; + } + #endif + }); + + router->map("permissions.request", [](auto message, auto router, auto reply) { + static auto userConfig = SSC::getUserConfig(); + auto err = validateMessageParameters(message, {"name"}); + + if (err.type != JSON::Type::Null) { + return reply(Result::Err { message, err }); + } + + auto name = message.get("name"); + + if (name == "geolocation") { + #if defined(__APPLE__) + auto performedActivation = [router->locationObserver attemptActivationWithCompletion: ^(BOOL isAuthorized) { + if (!isAuthorized) { + auto reason = @("Location observer could not be activated"); + + if (!router->locationObserver.locationManager) { + reason = @("Location observer manager is not initialized"); + } else if (!router->locationObserver.locationManager.location) { + reason = @("Location observer manager could not provide location"); + } + + auto error = [NSError + errorWithDomain: @(userConfig["bundle_identifier"].c_str()) + code: -1 + userInfo: @{ + NSLocalizedDescriptionKey: reason + } + ]; + } + + if (isAuthorized) { + auto data = JSON::Object::Entries {{"state", "granted"}}; + return reply(Result::Data { message, data }); + } else if (router->locationObserver.locationManager.authorizationStatus == kCLAuthorizationStatusNotDetermined) { + auto data = JSON::Object::Entries {{"state", "prompt"}}; + return reply(Result::Data { message, data }); + } else { + auto data = JSON::Object::Entries {{"state", "denied"}}; + return reply(Result::Data { message, data }); + } + }]; + + if (!performedActivation) { + auto err = JSON::Object::Entries {{ "message", "Location observer could not be activated" }}; + err["type"] = "GeolocationPositionError"; + return reply(Result::Err { message, err }); + } + + return; + #endif + } + + if (name == "notifications") { + #if defined(__APPLE__) + UNAuthorizationOptions options = UNAuthorizationOptionProvisional; + auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + auto requestAlert = message.get("alert") == "true"; + auto requestBadge = message.get("badge") == "true"; + auto requestSound = message.get("sound") == "true"; + + if (requestAlert) { + options |= UNAuthorizationOptionAlert; + } + + if (requestBadge) { + options |= UNAuthorizationOptionBadge; + } + + if (requestSound) { + options |= UNAuthorizationOptionSound; + } + + if (requestAlert && requestSound) { + options |= UNAuthorizationOptionCriticalAlert; + } + + [notificationCenter + requestAuthorizationWithOptions: options + completionHandler: ^(BOOL granted, NSError *error) { + [notificationCenter + getNotificationSettingsWithCompletionHandler: ^(UNNotificationSettings *settings) { + if (settings.authorizationStatus == UNAuthorizationStatusDenied) { + auto data = JSON::Object::Entries {{"state", "denied"}}; + return reply(Result::Data { message, data }); + } else if (settings.authorizationStatus == UNAuthorizationStatusNotDetermined) { + if (error) { + auto err = JSON::Object::Entries { + { "message", String(error.localizedFailureReason.UTF8String) } + }; + + return reply(Result::Err { message, err }); + } + + auto data = JSON::Object::Entries { + {"state", granted ? "granted" : "denied" } + }; + + return reply(Result::Data { message, data }); + } + + auto data = JSON::Object::Entries {{"state", "granted"}}; + return reply(Result::Data { message, data }); + }]; + }]; + #endif + } + }); + /** * Simply returns `pong`. */ @@ -2225,16 +2663,22 @@ static void registerSchemeHandler (Router *router) { - (id) init { self = [super init]; self.delegate = [[SSCLocationManagerDelegate alloc] initWithLocationObserver: self]; - self.isActivated = NO; + self.isAuthorized = NO; self.locationWatchers = [NSMutableArray new]; self.activationCompletions = [NSMutableArray new]; self.locationRequestCompletions = [NSMutableArray new]; - if ([CLLocationManager locationServicesEnabled]) { - self.locationManager = [[CLLocationManager alloc] init]; - self.locationManager.delegate = self.delegate; - self.locationManager.desiredAccuracy = CLAccuracyAuthorizationFullAccuracy; + self.locationManager = [CLLocationManager new]; + self.locationManager.delegate = self.delegate; + self.locationManager.desiredAccuracy = CLAccuracyAuthorizationFullAccuracy; + self.locationManager.pausesLocationUpdatesAutomatically = NO; +#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + self.locationManager.allowsBackgroundLocationUpdates = YES; + self.locationManager.showsBackgroundLocationIndicator = YES; +#endif + + if ([CLLocationManager locationServicesEnabled]) { if ( #if !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR self.locationManager.authorizationStatus == kCLAuthorizationStatusAuthorized || @@ -2243,7 +2687,7 @@ static void registerSchemeHandler (Router *router) { #endif self.locationManager.authorizationStatus == kCLAuthorizationStatusAuthorizedAlways ) { - self.isActivated = YES; + self.isAuthorized = YES; } } @@ -2255,7 +2699,8 @@ static void registerSchemeHandler (Router *router) { return NO; } - if (self.isActivated) { + if (self.isAuthorized) { + [self.locationManager requestLocation]; return YES; } @@ -2264,11 +2709,12 @@ static void registerSchemeHandler (Router *router) { #else [self.locationManager requestAlwaysAuthorization]; #endif + return YES; } - (BOOL) attemptActivationWithCompletion: (void (^)(BOOL)) completion { - if (self.isActivated) { + if (self.isAuthorized) { dispatch_async(dispatch_get_main_queue(), ^{ completion(YES); }); @@ -2284,9 +2730,9 @@ static void registerSchemeHandler (Router *router) { } - (BOOL) getCurrentPositionWithCompletion: (void (^)(NSError*, CLLocation*)) completion { - return [self attemptActivationWithCompletion: ^(BOOL isActivated) { + return [self attemptActivationWithCompletion: ^(BOOL isAuthorized) { static auto userConfig = SSC::getUserConfig(); - if (!isActivated) { + if (!isAuthorized) { auto reason = @("Location observer could not be activated"); if (!self.locationManager) { @@ -2306,7 +2752,12 @@ static void registerSchemeHandler (Router *router) { return completion(error, nullptr); } - completion(nullptr, self.locationManager.location); + auto location = self.locationManager.location; + if (location.timestamp.timeIntervalSince1970 > 0) { + completion(nullptr, self.locationManager.location); + } else { + [self.locationRequestCompletions addObject: [completion copy]]; + } [self.locationManager requestLocation]; }]; @@ -2333,9 +2784,9 @@ static void registerSchemeHandler (Router *router) { }]; } - auto performedActivation = [self attemptActivationWithCompletion: ^(BOOL isActivated) { + auto performedActivation = [self attemptActivationWithCompletion: ^(BOOL isAuthorized) { static auto userConfig = SSC::getUserConfig(); - if (!isActivated) { + if (!isAuthorized) { auto error = [NSError errorWithDomain: @(userConfig["bundle_identifier"].c_str()) code: -1 @@ -2348,6 +2799,12 @@ static void registerSchemeHandler (Router *router) { } [self.locationManager startUpdatingLocation]; + + if (CLLocationManager.headingAvailable) { + [self.locationManager startUpdatingHeading]; + } + + [self.locationManager startMonitoringSignificantLocationChanges]; }]; if (!performedActivation) { @@ -2407,19 +2864,23 @@ static void registerSchemeHandler (Router *router) { - (void) locationManager: (CLLocationManager*) locationManager didFailWithError: (NSError*) error { // TODO(@jwerle): handle location manager error + debug("locationManager:didFailWithError: %@", error); } - (void) locationManager: (CLLocationManager*) locationManager didFinishDeferredUpdatesWithError: (NSError*) error { + debug("locationManager:didFinishDeferredUpdatesWithError: %@", error); // TODO(@jwerle): handle deferred error } - (void) locationManagerDidPauseLocationUpdates: (CLLocationManager*) locationManager { // TODO(@jwerle): handle pause for updates + debug("locationManagerDidPauseLocationUpdates"); } - (void) locationManagerDidResumeLocationUpdates: (CLLocationManager*) locationManager { - // TODO(@jwerle): handle resume for updates + // TODO(@jwerle): handle resume for updates + debug("locationManagerDidResumeLocationUpdates"); } - (void) locationManager: (CLLocationManager*) locationManager @@ -2428,6 +2889,12 @@ static void registerSchemeHandler (Router *router) { [self locationManager: locationManager didUpdateLocations: locations]; } +- (void) locationManager: (CLLocationManager*) locationManager + didChangeAuthorizationStatus: (CLAuthorizationStatus) status { + // XXX(@jwerle): this is a legacy callback + [self locationManagerDidChangeAuthorization: locationManager]; +} + - (void) locationManagerDidChangeAuthorization: (CLLocationManager*) locationManager { auto activationCompletions = [NSArray arrayWithArray: self.locationObserver.activationCompletions]; if ( @@ -2438,7 +2905,13 @@ static void registerSchemeHandler (Router *router) { #endif locationManager.authorizationStatus == kCLAuthorizationStatusAuthorizedAlways ) { - self.locationObserver.isActivated = YES; + JSON::Object json = JSON::Object::Entries { + {"name", "geolocation"}, + {"state", "granted"} + }; + + self.locationObserver.router->emit("permissionchange", json.str()); + self.locationObserver.isAuthorized = YES; for (id item in activationCompletions) { auto completion = (void (^)(BOOL)) item; completion(YES); @@ -2448,7 +2921,16 @@ static void registerSchemeHandler (Router *router) { #endif } } else { - self.locationObserver.isActivated = NO; + JSON::Object json = JSON::Object::Entries { + {"name", "geolocation"}, + {"state", locationManager.authorizationStatus == kCLAuthorizationStatusNotDetermined + ? "prompt" + : "denied" + } + }; + + self.locationObserver.router->emit("permissionchange", json.str()); + self.locationObserver.isAuthorized = NO; for (id item in activationCompletions) { auto completion = (void (^)(BOOL)) item; completion(NO); @@ -2460,6 +2942,96 @@ static void registerSchemeHandler (Router *router) { } } @end + +@implementation SSCUserNotificationCenterDelegate +- (void) userNotificationCenter: (UNUserNotificationCenter*) center + didReceiveNotificationResponse: (UNNotificationResponse*) response + withCompletionHandler: (void (^)(void)) completionHandler { + completionHandler(); + Lock lock(notificationRouterMapMutex); + auto id = String(response.notification.request.identifier.UTF8String); + Router* router = notificationRouterMap.find(id) != notificationRouterMap.end() + ? notificationRouterMap.at(id) + : nullptr; + + if (router) { + JSON::Object json = JSON::Object::Entries { + {"id", id}, + {"action", + [response.actionIdentifier isEqualToString: UNNotificationDefaultActionIdentifier] + ? "default" + : "dismiss" + } + }; + + notificationRouterMap.erase(id); + router->emit("notificationresponse", json.str()); + } +} + +- (void) userNotificationCenter: (UNUserNotificationCenter*) center + willPresentNotification: (UNNotification*) notification + withCompletionHandler: (void (^)(UNNotificationPresentationOptions options)) completionHandler { + UNNotificationPresentationOptions options = UNNotificationPresentationOptionList; + + if (notification.request.content.sound != nullptr) { + options |= UNNotificationPresentationOptionSound; + } + + if (notification.request.content.attachments != nullptr) { + if (notification.request.content.attachments.count > 0) { + options |= UNNotificationPresentationOptionBanner; + } + } + + completionHandler(options); + + Lock lock(notificationRouterMapMutex); + auto __block id = String(notification.request.identifier.UTF8String); + Router* __block router = notificationRouterMap.find(id) != notificationRouterMap.end() + ? notificationRouterMap.at(id) + : nullptr; + + if (router) { + JSON::Object json = JSON::Object::Entries { + {"id", id} + }; + + router->emit("notificationpresented", json.str()); + // look for dismissed notification + auto timer = [NSTimer timerWithTimeInterval: 2 repeats: YES block: ^(NSTimer* timer) { + auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + [notificationCenter getDeliveredNotificationsWithCompletionHandler: ^(NSArray<UNNotification*> *notifications) { + BOOL found = NO; + + for (UNNotification* notification in notifications) { + if (String(notification.request.identifier.UTF8String) == id) { + return; + } + } + + [timer invalidate]; + JSON::Object json = JSON::Object::Entries { + {"id", id}, + {"action", "dismiss"} + }; + + router->emit("notificationresponse", json.str()); + + Lock lock(notificationRouterMapMutex); + if (notificationRouterMap.contains(id)) { + notificationRouterMap.erase(id); + } + }]; + }]; + + [NSRunLoop.mainRunLoop + addTimer: timer + forMode: NSDefaultRunLoopMode + ]; + } +} +@end #endif namespace SSC::IPC { @@ -2565,8 +3137,11 @@ namespace SSC::IPC { } Router::Router () { + static auto userConfig = SSC::getUserConfig(); + initRouterTable(this); registerSchemeHandler(this); + #if defined(__APPLE__) this->networkStatusObserver = [SSCIPCNetworkStatusObserver new]; this->locationObserver = [SSCLocationObserver new]; @@ -2581,6 +3156,51 @@ namespace SSC::IPC { #if defined(__APPLE__) [this->networkStatusObserver start]; + + if (userConfig["permissions_allow_notifications"] != "false") { + auto notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; + + if (!notificationCenter.delegate) { + notificationCenter.delegate = [SSCUserNotificationCenterDelegate new]; + } + + UNAuthorizationStatus __block currentAuthorizationStatus; + [notificationCenter getNotificationSettingsWithCompletionHandler: ^(UNNotificationSettings *settings) { + currentAuthorizationStatus = settings.authorizationStatus; + this->notificationPollTimer = [NSTimer timerWithTimeInterval: 2 repeats: YES block: ^(NSTimer* timer) { + // look for authorization status changes + [notificationCenter getNotificationSettingsWithCompletionHandler: ^(UNNotificationSettings *settings) { + if (currentAuthorizationStatus != settings.authorizationStatus) { + JSON::Object json; + currentAuthorizationStatus = settings.authorizationStatus; + if (settings.authorizationStatus == UNAuthorizationStatusDenied) { + json = JSON::Object::Entries { + {"name", "notifications"}, + {"state", "denied"} + }; + } else if (settings.authorizationStatus == UNAuthorizationStatusNotDetermined) { + json = JSON::Object::Entries { + {"name", "notifications"}, + {"state", "prompt"} + }; + } else { + json = JSON::Object::Entries { + {"name", "notifications"}, + {"state", "granted"} + }; + } + + this->emit("permissionchange", json.str()); + } + }]; + }]; + + [NSRunLoop.mainRunLoop + addTimer: this->notificationPollTimer + forMode: NSDefaultRunLoopMode + ]; + }]; + } #endif } @@ -2604,7 +3224,16 @@ namespace SSC::IPC { #endif } + if (this->notificationPollTimer) { + [this->notificationPollTimer invalidate]; + #if !__has_feature(objc_arc) + [this->notificationPollTimer release]; + #endif + } + + this->notificationPollTimer = nullptr; this->networkStatusObserver = nullptr; + this->locationObserver = nullptr; this->schemeHandler = nullptr; #endif } @@ -2890,12 +3519,5 @@ namespace SSC::IPC { - (void) start { nw_path_monitor_start(_monitor); } - -- (void) userNotificationCenter: (UNUserNotificationCenter *) center - willPresentNotification: (UNNotification *) notification - withCompletionHandler: (void (^)(UNNotificationPresentationOptions options)) completionHandler -{ - completionHandler(UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner); -} @end #endif From 2065e1275dfff95e9d823336b0c9c6c8b73c11c8 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:41:01 -0400 Subject: [PATCH 114/256] refactor(src/android/main.kt): handle notifications in main activity --- src/android/main.kt | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/android/main.kt b/src/android/main.kt index f47e554d04..8aa5fa8577 100644 --- a/src/android/main.kt +++ b/src/android/main.kt @@ -34,6 +34,7 @@ class PermissionRequest (callback: (Boolean) -> Unit) { */ open class MainActivity : WebViewActivity() { override open protected val TAG = "Mainctivity" + open lateinit var notificationChannel: android.app.NotificationChannel open lateinit var runtime: Runtime open lateinit var window: Window @@ -80,6 +81,12 @@ open class MainActivity : WebViewActivity() { super.onCreate(state) + this.notificationChannel = android.app.NotificationChannel( + "__BUNDLE_IDENTIFIER__", + "__BUNDLE_IDENTIFIER__ Notifications", + android.app.NotificationManager.IMPORTANCE_DEFAULT + ) + this.runtime = Runtime(this, RuntimeConfiguration( assetManager = this.applicationContext.resources.assets, rootDirectory = this.getRootDirectory(), @@ -101,6 +108,11 @@ open class MainActivity : WebViewActivity() { this.window.load() this.runtime.start() + + if (this.runtime.isPermissionAllowed("notifications")) { + val notificationManager = this.getSystemService(NOTIFICATION_SERVICE) as android.app.NotificationManager + notificationManager.createNotificationChannel(this.notificationChannel) + } } override fun onStart () { @@ -128,6 +140,33 @@ open class MainActivity : WebViewActivity() { return super.onDestroy() } + override fun onNewIntent (intent: android.content.Intent) { + super.onNewIntent(intent) + val window = this.window + val action = intent.action + val id = intent.extras?.getCharSequence("id")?.toString() + + if (action != null) { + if (action == "notification.response.default") { + this.runOnUiThread { + window.bridge.emit("notificationresponse", """{ + "id": "$id", + "action": "default" + }""") + } + } + + if (action == "notification.response.dismiss") { + this.runOnUiThread { + window.bridge.emit("notificationresponse", """{ + "id": "$id", + "action": "dismiss" + }""") + } + } + } + } + override fun onPageStarted ( view: android.webkit.WebView, url: String, From e50fbf9a9e282c48778102a269281b49f857cfaa Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:41:16 -0400 Subject: [PATCH 115/256] refactor(src/android/bridge.cc): introduce 'emit()' binding --- src/android/bridge.cc | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/android/bridge.cc b/src/android/bridge.cc index 286db8c177..359f783bfa 100644 --- a/src/android/bridge.cc +++ b/src/android/bridge.cc @@ -189,4 +189,32 @@ extern "C" { return routed; } + + jboolean external(Bridge, emit)( + JNIEnv *env, + jobject self, + jstring eventNameString, + jstring eventDataString + ) { + auto bridge = Bridge::from(env, self); + + if (bridge == nullptr) { + Throw(env, BridgeNotInitializedException); + return false; + } + + JavaVM* jvm = nullptr; + auto jniVersion = env->GetVersion(); + env->GetJavaVM(&jvm); + auto attachment = JNIEnvironmentAttachment { jvm, jniVersion }; + + if (attachment.hasException()) { + return false; + } + + auto event = StringWrap(env, eventNameString); + auto data = StringWrap(env, eventDataString); + + return bridge->router.emit(event.str(), data.str()); + } } From 9359089865e88dd3dc538ef92cd969894104011d Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:41:55 -0400 Subject: [PATCH 116/256] feat(src/android/bridge.kt): introduce notification support --- src/android/bridge.kt | 233 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 230 insertions(+), 3 deletions(-) diff --git a/src/android/bridge.kt b/src/android/bridge.kt index 76bab65d85..23708a0b1c 100644 --- a/src/android/bridge.kt +++ b/src/android/bridge.kt @@ -164,7 +164,14 @@ open class Bridge (runtime: Runtime, configuration: IBridgeConfiguration) { when (message.command) { "permissions.request" -> { - val name = message.get("name") ?: return false + if (!message.has("name")) { + callback(Result(0, message.seq, message.command, """{ + "err": { "message": "Expecting 'name' in parameters" } + }""")) + return true + } + + val name = message.get("name") val permissions = mutableListOf<String>() when (name) { @@ -216,7 +223,15 @@ open class Bridge (runtime: Runtime, configuration: IBridgeConfiguration) { } "permissions.query" -> { - val name = message.get("name") ?: return false + if (!message.has("name")) { + callback(Result(0, message.seq, message.command, """{ + "err": { "message": "Expecting 'name' in parameters" } + }""")) + return true + } + + val name = message.get("name") + if (name == "geolocation") { if (!runtime.isPermissionAllowed("geolocation")) { callback(Result(0, message.seq, message.command, """{ @@ -250,7 +265,8 @@ open class Bridge (runtime: Runtime, configuration: IBridgeConfiguration) { } }""")) } else if ( - activity.checkPermission("android.permission.POST_NOTIFICATIONS") + activity.checkPermission("android.permission.POST_NOTIFICATIONS") && + androidx.core.app.NotificationManagerCompat.from(activity).areNotificationsEnabled() ) { callback(Result(0, message.seq, message.command, """{ "data": { @@ -279,6 +295,214 @@ open class Bridge (runtime: Runtime, configuration: IBridgeConfiguration) { return true } + "notification.show" -> { + if ( + !activity.checkPermission("android.permission.POST_NOTIFICATIONS") || + !androidx.core.app.NotificationManagerCompat.from(activity).areNotificationsEnabled() + ) { + callback(Result(0, message.seq, message.command, """{ + "err": { "message": "User denied permissions for 'notifications'" } + }""")) + return true + } + + if (!message.has("id")) { + callback(Result(0, message.seq, message.command, """{ + "err": { "message": "Expecting 'id' in parameters" } + }""")) + return true + } + + if (!message.has("title")) { + callback(Result(0, message.seq, message.command, """{ + "err": { "message": "Expecting 'title' in parameters" } + }""")) + return true + } + + val id = message.get("id") + val channel = message.get("channel", "default").replace("default", "__BUNDLE_IDENTIFIER__"); + val vibrate = message.get("vibrate") + .split(",") + .filter({ it.length > 0 }) + .map({ it.toInt().toLong() }) + .toTypedArray() + + val identifier = id.toLongOrNull()?.toInt() ?: (0..16384).random().toInt() + + val contentIntent = android.content.Intent(activity, MainActivity::class.java).apply { + flags = ( + android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP or + android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP + ) + } + + val deleteIntent = android.content.Intent(activity, MainActivity::class.java).apply { + flags = ( + android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP or + android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP + ) + } + + contentIntent.setAction("notification.response.default") + contentIntent.putExtra("id", id) + + deleteIntent.setAction("notification.response.dismiss") + deleteIntent.putExtra("id", id) + + val pendingContentIntent: android.app.PendingIntent = android.app.PendingIntent.getActivity( + activity, + identifier, + contentIntent, + ( + android.app.PendingIntent.FLAG_UPDATE_CURRENT or + android.app.PendingIntent.FLAG_IMMUTABLE or + android.app.PendingIntent.FLAG_ONE_SHOT + ) + ) + + val pendingDeleteIntent: android.app.PendingIntent = android.app.PendingIntent.getActivity( + activity, + identifier, + deleteIntent, + ( + android.app.PendingIntent.FLAG_UPDATE_CURRENT or + android.app.PendingIntent.FLAG_IMMUTABLE + ) + ) + + val builder = androidx.core.app.NotificationCompat.Builder( + activity, + channel + ) + + builder + .setPriority(androidx.core.app.NotificationCompat.PRIORITY_DEFAULT) + .setContentTitle(message.get("title", "Notification")) + .setContentIntent(pendingContentIntent) + .setDeleteIntent(pendingDeleteIntent) + .setAutoCancel(true) + + if (message.has("body")) { + builder.setContentText(message.get("body")) + } + + if (message.has("icon")) { + val url = message.get("icon") + .replace("socket://__BUNDLE_IDENTIFIER__", "https://appassets.androidplatform.net/assets") + .replace("https://__BUNDLE_IDENTIFIER__", "https://appassets.androidplatform.net/assets") + + val icon = androidx.core.graphics.drawable.IconCompat.createWithContentUri(url) + builder.setSmallIcon(icon) + } else { + val icon = androidx.core.graphics.drawable.IconCompat.createWithResource( + activity, + R.mipmap.ic_launcher_round + ) + builder.setSmallIcon(icon) + } + + if (message.has("image")) { + val url = message.get("image") + .replace("socket://__BUNDLE_IDENTIFIER__", "https://appassets.androidplatform.net/assets") + .replace("https://__BUNDLE_IDENTIFIER__", "https://appassets.androidplatform.net/assets") + + val icon = android.graphics.drawable.Icon.createWithContentUri(url) + builder.setLargeIcon(icon) + } + + if (message.has("category")) { + var category = message.get("category") + .replace("msg", "message") + .replace("-", "_") + + builder.setCategory(category) + } + + if (message.get("silent") == "true") { + builder.setSilent(true) + } + + val notification = builder.build() + with (androidx.core.app.NotificationManagerCompat.from(activity)) { + notify( + message.get("tag"), + identifier, + notification + ) + } + + callback(Result(0, message.seq, message.command, """{ + "data": { + "id": "$id" + } + }""")) + + activity.runOnUiThread { + this.emit("notificationpresented", """{ + "id": "$id" + }""") + } + + return true + } + + "notification.close" -> { + if ( + !activity.checkPermission("android.permission.POST_NOTIFICATIONS") || + !androidx.core.app.NotificationManagerCompat.from(activity).areNotificationsEnabled() + ) { + callback(Result(0, message.seq, message.command, """{ + "err": { "message": "User denied permissions for 'notifications'" } + }""")) + return true + } + + if (!message.has("id")) { + callback(Result(0, message.seq, message.command, """{ + "err": { "message": "Expecting 'id' in parameters" } + }""")) + return true + } + + val id = message.get("id") + with (androidx.core.app.NotificationManagerCompat.from(activity)) { + cancel( + message.get("tag"), + id.toLongOrNull()?.toInt() ?: (0..16384).random().toInt() + ) + } + + callback(Result(0, message.seq, message.command, """{ + "data": { + "id": "$id" + } + }""")) + + activity.runOnUiThread { + this.emit("notificationresponse", """{ + "id": "$id", + "action": "dismiss" + }""") + } + + return true + } + + "notification.list" -> { + if ( + !activity.checkPermission("android.permission.POST_NOTIFICATIONS") || + !androidx.core.app.NotificationManagerCompat.from(activity).areNotificationsEnabled() + ) { + callback(Result(0, message.seq, message.command, """{ + "err": { "message": "User denied permissions for 'notifications'" } + }""")) + return true + } + + return true + } + "buffer.map" -> { if (bytes != null) { buffers[message.seq] = bytes @@ -400,4 +624,7 @@ open class Bridge (runtime: Runtime, configuration: IBridgeConfiguration) { @Throws(java.lang.Exception::class) external fun route (msg: String, bytes: ByteArray?, requestId: Long): Boolean; + + @Throws(java.lang.Exception::class) + external fun emit (event: String, data: String = ""): Boolean; } From 5007514474b527f2af1cffa8bd08fd6ef9aa2f1c Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:42:08 -0400 Subject: [PATCH 117/256] fix(api/ipc.js): fix typo --- api/ipc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/ipc.js b/api/ipc.js index 0d747ee045..44728842f1 100644 --- a/api/ipc.js +++ b/api/ipc.js @@ -171,7 +171,7 @@ function getErrorClass (type, fallback) { if (typeof globalThis !== 'undefined' && typeof globalThis[type] === 'function') { // eslint-disable-next-line return new Function(`return function ${type} () { - const object = Object.create(globalThis[type].prototype, { + const object = Object.create(globalThis[${type}].prototype, { code: { value: null } }) From a0b0bd7fa5085ed99dd06d6979ade1c9ef4c106c Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:42:25 -0400 Subject: [PATCH 118/256] chore(api/internal/geolocation.js): clean up docs --- api/internal/geolocation.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/api/internal/geolocation.js b/api/internal/geolocation.js index 9ff5a4af97..e99c6f6480 100644 --- a/api/internal/geolocation.js +++ b/api/internal/geolocation.js @@ -125,6 +125,7 @@ export const platform = { } /** + * Get the current position of the device. * @param {function(GeolocationPosition)} onSuccess * @param {onError(Error)} onError * @param {object=} options @@ -211,11 +212,14 @@ export async function getCurrentPosition ( } /** + * Register a handler function that will be called automatically each time the + * position of the device changes. You can also, optionally, specify an error + * handling callback function. * @param {function(GeolocationPosition)} onSuccess * @param {function(Error)} onError - * @param {object=} options - * @param {number=} options.timeout - * @return {Promise} + * @param {object=} [options] + * @param {number=} [options.timeout = null] + * @return {number} */ export function watchPosition ( onSuccess, @@ -300,6 +304,11 @@ export function watchPosition ( return identifier } +/** + * Unregister location and error monitoring handlers previously installed + * using `watchPosition`. + * @param {number} id + */ export function clearWatch (id) { if (!isApple) { return platform.clearWatch(...arguments) From c64b40f2923df9ccb2396139644752b9a40edc84 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:42:51 -0400 Subject: [PATCH 119/256] refactor(api/internal/permissions.js): improve permissions lifecycle --- api/internal/permissions.js | 255 ++++++++++++++++++++++++++++++------ 1 file changed, 215 insertions(+), 40 deletions(-) diff --git a/api/internal/permissions.js b/api/internal/permissions.js index 048cfed12c..e6c91bb96c 100644 --- a/api/internal/permissions.js +++ b/api/internal/permissions.js @@ -1,39 +1,146 @@ /* global EventTarget, Event */ +/** + * @module Permissions + * This module provides an API for querying and requesting permissions. + */ import { IllegalConstructorError } from '../errors.js' +import Enumeration from '../enumeration.js' +import hooks from '../hooks.js' import ipc from '../ipc.js' +import gc from '../gc.js' import os from '../os.js' +/** + * @typedef {{ name: string }} PermissionDescriptor + */ + const isAndroid = os.platform() === 'android' +const isApple = os.platform() === 'darwin' +/** + * Get a bound platform `navigator.permissions` function. + * @ignore + * @param {string} name} + * @return {function} + */ +function getPlatformFunction (name) { + if (!globalThis.window?.navigator?.permissions?.[name]) return null + const value = globalThis.window.navigator.permissions[name] + return value.bind(globalThis.navigator.permissions) +} + +/** + * Native platform functions + * @ignore + */ +const platform = { + query: getPlatformFunction('query') +} + +/** + * An enumeration of the permission types. + * - 'geolocation' + * - 'notifications' + * - 'push' + * - 'persistent-storage' + * - 'midi' + * - 'storage-access' + * @type {Enumeration} + * @ignore + */ +export const types = Enumeration.from([ + 'geolocation', + 'notifications', + 'push', + 'persistent-storage', + 'midi', + 'storage-access' +]) + +/** + * A container that provides the state of an object and an event handler + * for monitoring changes permission changes. + * @ignore + */ class PermissionStatus extends EventTarget { + #removePermissionChangeListener = null + #subscribed = true #onchange = null + #signal = null #state = null #name = null - - constructor (name, subscribe) { + /** + * `PermissionStatus` class constructor. + * @param {string} name + * @param {string} initialState + * @param {object=} [options] + * @param {?AbortSignal} [options.signal = null] + */ + constructor (name, initialState, options = { signal: null }) { super() this.#name = name - subscribe((state) => { - if (this.#state !== state) { - this.#state = state + this.#state = initialState + this.#signal = options?.signal ?? null + + this.#removePermissionChangeListener = hooks.onPermissionChange((event) => { + const { detail } = event + if (this.#subscribed === false) { + this.#removePermissionChangeListener() + this.#removePermissionChangeListener = null + return + } + + if (detail.name === name && detail.state !== this.#state) { + this.#state = detail.state this.dispatchEvent(new Event('change')) } }) + + if (this.#signal?.aborted === true) { + this.removePermissionChangeListener() + } + + if (typeof this.#signal?.addEventListener === 'function') { + this.#signal.addEventListener('abort', () => { + this.#removePermissionChangeListener() + this.#removePermissionChangeListener = null + this.unsubscribe() + }, { once: true }) + } + + gc.ref(this) } + /** + * String tag for `PermissionStatus`. + * @ignore + */ get [Symbol.toStringTag] () { return 'PermissionStatus' } + /** + * The name of this permission this status is for. + * @type {string} + */ get name () { return this.#name } + /** + * The current state of the permission status. + * @type {string} + */ get state () { return this.#state } + /** + * Level 0 event target 'change' event listener accessor + * @type {function(Event)} + */ + get onchange () { return this.#onchange } set onchange (onchange) { if (typeof this.#onchange === 'function') { this.removeEventListener('change', this.#onchange) @@ -45,44 +152,42 @@ class PermissionStatus extends EventTarget { } } - get onchange () { - return this.#onchange + /** + * Non-standard method for unsubscribing to status state updates. + * @ignore + */ + unsubscribe () { + this.#subscribed = false } -} - -/** - * @ignore - * @param {string} name} - * @return {function} - */ -function getPlatformFunction (name) { - if (!globalThis.window?.navigator?.permissions?.[name]) return null - const value = globalThis.window.navigator.permissions[name] - return value.bind(globalThis.navigator.permissions) -} -const platform = { - query: getPlatformFunction('query') + /** + * Implements `gc.finalizer` for gc'd resource cleanup. + * @return {gc.Finalizer} + * @ignore + */ + [gc.finalizer] () { + return { + args: [this.removePermissionChangeListener], + handle (removePermissionChangeListener) { + console.log('GCd') + removePermissionChangeListener() + } + } + } } /** - * @param {{ name: string }} descriptor + * Query for a permission status. + * @param {PermissionDescriptor} descriptor + * @param {object=} [options] + * @param {?AbortSignal} [options.signal = null] * @return {Promise<PermissionStatus>} */ -export async function query (descriptor) { - if (!isAndroid) { +export async function query (descriptor, options) { + if (!isAndroid && !isApple) { return platform.query(descriptor) } - const types = [ - 'geolocation', - 'notifications', - 'push', - 'persistent-storage', - 'midi', - 'storage-access' - ] - if (arguments.length === 0) { throw new TypeError( 'Failed to execute \'query\' on \'Permissions\': ' + @@ -107,7 +212,7 @@ export async function query (descriptor) { ) } - if (typeof name !== 'string' || name.length === 0 || !types.includes(name)) { + if (typeof name !== 'string' || name.length === 0 || !types.contains(name)) { throw new TypeError( 'Failed to execute \'query\' on \'Permissions\': ' + 'Failed to read the \'name\' property from \'PermissionDescriptor\': ' + @@ -115,22 +220,92 @@ export async function query (descriptor) { ) } - const result = await ipc.send('permissions.query', { name }) - const status = new PermissionStatus(name, async (update) => { - queueMicrotask(() => update(result.data.state)) - }) + const result = await ipc.send('permissions.query', { name, signal: options?.signal }) if (result.err) { throw result.err } - return status + return new PermissionStatus(name, result.data.state, options) } +/** + * Request a permission to be granted. + * @param {PermissionDescriptor} descriptor + * @param {object=} [options] + * @param {?AbortSignal} [options.signal = null] + * @return {Promise<PermissionStatus>} + */ +export async function request (descriptor, options) { + if (arguments.length === 0) { + throw new TypeError( + 'Failed to execute \'request\' on \'Permissions\': ' + + '1 argument required, but only 0 present.' + ) + } + + if (!descriptor || typeof descriptor !== 'object') { + throw new TypeError( + 'Failed to execute \'request\' on \'Permissions\': ' + + 'parameter 1 is not of type \'object\'.' + ) + } + + const { name } = descriptor + + if (name === undefined) { + throw new TypeError( + 'Failed to execute \'request\' on \'Permissions\': ' + + 'Failed to read the \'name\' property from \'PermissionDescriptor\': ' + + 'Required member is undefined.' + ) + } + + if (typeof name !== 'string' || name.length === 0 || !types.contains(name)) { + throw new TypeError( + 'Failed to execute \'request\' on \'Permissions\': ' + + 'Failed to read the \'name\' property from \'PermissionDescriptor\': ' + + `The provided value '${name}' is not a valid enum value of type PermissionName.` + ) + } + + const result = await ipc.send('permissions.request', { name, signal: options?.signal }) + + if (result.err) { + throw result.err + } + + return new PermissionStatus(name, result.data.state, options) +} + +/** + * An interface for querying and revoking permissions. + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Permissions} + */ class Permissions { + /** + * Returns the state of a user permission on the global scope. + * @type {function(PermissionDescriptor): Promise<PermissionStatus>} + */ + query = query + + /** + * Request a permission to be granted. + * @type {function(PermissionDescriptor): Promise<PermissionStatus>} + */ + request = request + + /** + * `Permissions` class constructor. This interface is + * not constructable. + * @ignore + */ constructor () { throw new IllegalConstructorError() } } -export default Object.assign(Object.create(Permissions.prototype), { query }) +export default Object.assign(Object.create(Permissions.prototype), { + query, + request +}) From a729d4d51f32b01eadd17bfca2c4d5c796e95e3c Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:43:30 -0400 Subject: [PATCH 120/256] refactor(api/notification.js): finish 'Notification' interface --- api/notification.js | 462 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 453 insertions(+), 9 deletions(-) diff --git a/api/notification.js b/api/notification.js index cda189f926..0eaae4501c 100644 --- a/api/notification.js +++ b/api/notification.js @@ -1,27 +1,93 @@ +/* global Event, ErrorEvent, EventTarget */ +/** + * @module Notification + * The Notification modules provides an API to configure and display + * desktop and mobile notifications to the user. It also includes mechanisms + * for request permissions to use notifications on the user's device. + */ import { Enumeration } from './enumeration.js' +import permissions from './internal/permissions.js' +import { rand64 } from './crypto.js' import language from './language.js' import location from './location.js' +import hooks from './hooks.js' import URL from './url.js' +import ipc from './ipc.js' +/** + * Used to determine if notification beign created in a `ServiceWorker`. + * @ignore + */ const isServiceWorkerGlobalScope = typeof globalThis.registration?.active === 'string' -export const NotificationDirection = new Enumeration(['auto', 'ltr', 'rtl']) - +/** + * Default number of max actions a notification can perform. + * @ignore + * @type {number} + */ +const DEFAULT_MAX_ACTIONS = 2 + +/** + * An enumeratino of notification test directions: + * - 'auto' Automatically determined by the operating system + * - 'ltr' Left-to-right text direction + * - 'rtl' Right-to-left text direction + * @type {Enumeration} + * @ignore + */ +export const NotificationDirection = Enumeration.from([ + 'auto', + 'ltr', + 'rtl' +]) + +/** + * An enumeration of permission types granted by the user for the current + * origin to display notifications to the end user. + * - 'granted' The user has explicitly granted permission for the current + * origin to display system notifications. + * - 'denied' The user has explicitly denied permission for the current + * origin to display system notifications. + * - 'default' The user decision is unknown; in this case the application + * will act as if permission was denied. + * @type {Enumeration} + * @ignore + */ +export const NotificationPermission = Enumeration.from([ + 'granted', + 'denied', + 'default' +]) + +/** + * A validated notification action object container. + * You should never need to construct this. + * @ignore + */ export class NotificationAction { #action = null #title = null #icon = null + + /** + * `NotificationAction` class constructor. + * @ignore + * @param {object} options + * @param {string} options.action + * @param {string} options.title + * @param {string|URL=} [options.icon = ''] + */ constructor (options) { if (options?.action === undefined) { throw new TypeError( - 'Failed to read the \'action\' property from ' + 'Failed to read the \'action\' property from ' + `'NotificationAction': Required member is ${options.action}.` ) } if (options?.title === undefined) { throw new TypeError( - 'Failed to read the \'title\' property from ' + 'Failed to read the \'title\' property from ' + `'NotificationAction': Required member is ${options.title}.` ) } @@ -31,11 +97,30 @@ export class NotificationAction { this.#icon = String(options.icon ?? '') } + /** + * A string identifying a user action to be displayed on the notification. + * @type {string} + */ get action () { return this.#action } + + /** + * A string containing action text to be shown to the user. + * @type {string} + */ get title () { return this.#title } + + /** + * A string containing the URL of an icon to display with the action. + * @type {string} + */ get icon () { return this.#icon } } +/** + * A validated notification options object container. + * You should never need to construct this. + * @ignore + */ export class NotificationOptions { #actions = [] #badge = '' @@ -43,6 +128,7 @@ export class NotificationOptions { #data = null #dir = 'auto' #icon = '' + #image = '' #lang = '' #renotify = false #requireInteraction = false @@ -50,7 +136,25 @@ export class NotificationOptions { #tag = '' #vibrate = [] - constructor (options) { + /** + * `NotificationOptions` class constructor. + * @ignore + * @param {object} [options = {}] + * @param {'auto'|'ltr|'rtl'=} [options.dir = 'auto'] + * @param {NotificationAction[]=} [options.actions = []] + * @param {string|URL=} [options.badge = ''] + * @param {string=} [options.body = ''] + * @param {?any=} [options.data = null] + * @param {string|URL=} [options.icon = ''] + * @param {string|URL=} [options.image = ''] + * @param {string=} [options.lang = ''] + * @param {string=} [options.tag = ''] + * @param {boolean=} [options.boolean = ''] + * @param {boolean=} [options.requireInteraction = false] + * @param {boolean=} [options.silent = false] + * @param {number[]=} [options.vibrate = []] + */ + constructor (options = {}) { if ('dir' in options) { if (!(options.dir in NotificationDirection)) { throw new TypeError( @@ -81,6 +185,10 @@ export class NotificationOptions { `'NotificationOptions': ${err.message}` ) } + + if (this.#actions.length === state.maxActions) { + break + } } } @@ -114,7 +222,7 @@ export class NotificationOptions { if ('lang' in options && options.lang !== undefined) { if (typeof options.lang === 'string' && options.lang.length > 2) { - this.#lang = language.describe(options.lang).[0]?.tag || '' + this.#lang = language.describe(options.lang)[0]?.tag || '' } } @@ -142,7 +250,7 @@ export class NotificationOptions { if ('vibrate' in options && options.vibrate !== undefined) { if (Array.isArray(options.vibrate)) { - this.#vibrate = this.#vibrate + this.#vibrate = options.vibrate } else if (options.vibrate) { this.#vibrate = [options.vibrate] } else { @@ -162,22 +270,163 @@ export class NotificationOptions { } } + /** + * An array of actions to display in the notification. + * @type {NotificationAction[]} + */ get actions () { return this.#actions } + + /** + * A string containing the URL of the image used to represent + * the notification when there isn't enough space to display the + * notification itself. + * @type {string} + */ get badge () { return this.#badge } + + /** + * A string representing the body text of the notification, + * which is displayed below the title. + * @type {string} + */ get body () { return this.#body } + + /** + * Arbitrary data that you want associated with the notification. + * This can be of any data type. + * @type {?any} + */ get data () { return this.#data } + + /** + * The direction in which to display the notification. + * It defaults to 'auto', which just adopts the environments + * language setting behavior, but you can override that behavior + * by setting values of 'ltr' and 'rtl'. + * @type {'auto'|'ltr'|'rtl'} + */ get dir () { return this.#dir } + + /** + * A string containing the URL of an icon to be displayed in the notification. + * @type {string} + */ get icon () { return this.#icon } + + /** + * The URL of an image to be displayed as part of the notification, as + * specified in the constructor's options parameter. + * @type {string} + */ + get image () { return this.#image } + + /** + * The notification's language, as specified using a string representing a + * language tag according to RFC 5646. + * @type {string} + */ get lang () { return this.#lang } + + /** + * A boolean value specifying whether the user should be notified after a + * new notification replaces an old one. The default is `false`, which means + * they won't be notified. If `true`, then tag also must be set. + * @type {boolean} + */ get renotify () { return this.#renotify } + + /** + * Indicates that a notification should remain active until the user clicks + * or dismisses it, rather than closing automatically. + * The default value is `false`. + * @type {boolean} + */ get requireInteraction () { return this.#requireInteraction } + + /** + * A boolean value specifying whether the notification is silent (no sounds + * or vibrations issued), regardless of the device settings. + * The default is `false`, which means it won't be silent. If `true`, then + * vibrate must not be present. + * @type {boolean} + */ get silent () { return this.#silent } + + /** + * A string representing an identifying tag for the notification. + * The default is the empty string. + * @type {string} + */ get tag () { return this.#tag } + + /** + * A vibration pattern for the device's vibration hardware to emit with + * the notification. If specified, silent must not be `true`. + * @type {number[]} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API#vibration_patterns} + */ get vibrate () { return this.#vibrate } } +/** + * Show a notification. Creates a `Notification` instance and displays + * it to the user. + * @param {string} title + * @param {NotificationOptions=} [options] + * @param {function(Event)=} [onclick] + * @param {function(Event)=} [onclose] + * @return {Promise} + */ +export async function showNotification (title, options, onclick = null, onshow = null) { + const notification = new Notification(title, options) + + await new Promise((resolve, reject) => { + notification.onclick = onclick + notification.onshow = onshow + notification.onerror = (e) => reject(e.error) + notification.onshow = () => resolve() + }) +} + +/** + * Internal state + * @ignore + */ +const state = { + permission: 'default', + maxActions: DEFAULT_MAX_ACTIONS, + pending: new Map() +} + +/** + * The Notification interface is used to configure and display + * desktop and mobile notifications to the user. + */ export class Notification extends EventTarget { + /** + * A read-only property that indicates the current permission granted + * by the user to display notifications. + * @type {'prompt'|'granted'|'denied'} + */ static get permission () { + return state.permission + } + + /** + * The maximum number of actions supported by the device. + * @type {number} + */ + static get maxActions () { + return state.maxActions + } + + /** + * Requests permission from the user to display notifications. + */ + static async requestPermission () { + const status = await permissions.request({ name: 'notifications' }) + status.unsubscribe() + return status.state } #onclick = null @@ -188,9 +437,16 @@ export class Notification extends EventTarget { #options = null #timestamp = Date.now() #title = null + #id = null + /** + * `Notification` class constructor. + * @param {string} title + * @param {NotificationOptions=} [options] + */ constructor (title, options = {}) { super() + if (arguments.length === 0) { throw new TypeError( 'Failed to construct \'Notification\': ' + @@ -218,8 +474,69 @@ export class Notification extends EventTarget { `Failed to construct 'Notification': ${err.message}` ) } + + this.#id = (rand64() & 0xFFFFn).toString() + + const request = ipc.request('notification.show', { + body: this.body, + icon: this.icon, + id: this.#id, + image: this.image, + lang: this.lang, + tag: this.tag || '', + title: this.title, + silent: this.silent + }) + + this[Symbol.for('Notification.request')] = request + + state.pending.set(this.id, this) + + const removeNotificationPresentedListener = hooks.onNotificationPresented((event) => { + if (event.detail.id === this.id) { + removeNotificationPresentedListener() + return this.dispatchEvent(new Event('show')) + } + }) + + const removeNotificationResponseListener = hooks.onNotificationResponse((event) => { + if (event.detail.id === this.id) { + const eventName = event.detail.action === 'dismiss' ? 'close' : 'click' + removeNotificationResponseListener() + this.dispatchEvent(new Event(eventName)) + if (eventName === 'click') { + queueMicrotask(() => this.dispatchEvent(new Event('close'))) + } + } + }) + + // propagate error to caller + request.then((result) => { + if (result.err) { + state.pending.delete(this.id, this) + removeNotificationPresentedListener() + removeNotificationResponseListener() + return this.dispatchEvent(new ErrorEvent('error', { + message: result.err.message, + error: result.err + })) + } + }) } + /** + * A unique identifier for this notification. + * @type {string} + */ + get id () { + return this.#id + } + + /** + * The click event is dispatched when the user clicks on + * displayed notification. + * @type {?function} + */ get onclick () { return this.#onclick } set onclick (onclick) { if (this.#onclick === onclick) { @@ -237,6 +554,10 @@ export class Notification extends EventTarget { } } + /** + * The close event is dispatched when the notification closes. + * @type {?function} + */ get onclose () { return this.#onclose } set onclose (onclose) { if (this.#onclose === onclose) { @@ -254,6 +575,11 @@ export class Notification extends EventTarget { } } + /** + * The eror event is dispatched when the notification fails to display + * or encounters an error. + * @type {?function} + */ get onerror () { return this.#onerror } set onerror (onerror) { if (this.#onerror === onerror) { @@ -271,6 +597,10 @@ export class Notification extends EventTarget { } } + /** + * The click event is dispatched when the notification is displayed. + * @type {?function} + */ get onshow () { return this.#onshow } set onshow (onshow) { if (this.#onshow === onshow) { @@ -288,20 +618,134 @@ export class Notification extends EventTarget { } } + /** + * An array of actions to display in the notification. + * @type {NotificationAction[]} + */ get actions () { return this.#options.actions } + + /** + * A string containing the URL of the image used to represent + * the notification when there isn't enough space to display the + * notification itself. + * @type {string} + */ get badge () { return this.#options.badge } + + /** + * A string representing the body text of the notification, + * which is displayed below the title. + * @type {string} + */ get body () { return this.#options.body } + + /** + * Arbitrary data that you want associated with the notification. + * This can be of any data type. + * @type {?any} + */ get data () { return this.#options.data } + + /** + * The direction in which to display the notification. + * It defaults to 'auto', which just adopts the environments + * language setting behavior, but you can override that behavior + * by setting values of 'ltr' and 'rtl'. + * @type {'auto'|'ltr'|'rtl'} + */ get dir () { return this.#options.dir } + + /** + * A string containing the URL of an icon to be displayed in the notification. + * @type {string} + */ get icon () { return this.#options.icon } + + /** + * The URL of an image to be displayed as part of the notification, as + * specified in the constructor's options parameter. + * @type {string} + */ + get image () { return this.#options.image } + + /** + * The notification's language, as specified using a string representing a + * language tag according to RFC 5646. + * @type {string} + */ get lang () { return this.#options.lang } + + /** + * A boolean value specifying whether the user should be notified after a + * new notification replaces an old one. The default is `false`, which means + * they won't be notified. If `true`, then tag also must be set. + * @type {boolean} + */ get renotify () { return this.#options.renotify } + + /** + * Indicates that a notification should remain active until the user clicks + * or dismisses it, rather than closing automatically. + * The default value is `false`. + * @type {boolean} + */ get requireInteraction () { return this.#options.requireInteraction } - get silent () { return this.#silent } + + /** + * A boolean value specifying whether the notification is silent (no sounds + * or vibrations issued), regardless of the device settings. + * The default is `false`, which means it won't be silent. If `true`, then + * vibrate must not be present. + * @type {boolean} + */ + get silent () { return this.#options.silent } + + /** + * A string representing an identifying tag for the notification. + * The default is the empty string. + * @type {string} + */ get tag () { return this.#options.tag } + + /** + * A vibration pattern for the device's vibration hardware to emit with + * the notification. If specified, silent must not be `true`. + * @type {number[]} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API#vibration_patterns} + */ + get vibrate () { return this.#options.vibrate } + + /** + * The timestamp of the notification. + * @type {number} + */ get timestamp () { return this.#timestamp } + + /** + * The title read-only property of the `Notification` instace indicates + * the title of the notification, as specified in the `title` parameter + * of the `Notification` constructor. + * @type {string} + */ get title () { return this.#title } - get vibrate () { return this.#options.vibrate } + + /** + * Closes the notification programmatically. + */ + async close () { + const result = await ipc.request('notification.close', { id: this.id }) + if (result.err) { + console.warn('Failed to close \'Notification\': %s', result.err.message) + } + } } +// listen for 'notification' permission changes where applicable +permissions.query({ name: 'notifications' }).then((result) => { + result.addEventListener('change', () => { + // 'prompt' -> 'default' + state.permission = result.state.replace('prompt', 'default') + }) +}) + export default Notification From d6cac1c6e86ab3e8925f88309067d538dc80bafe Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:43:45 -0400 Subject: [PATCH 121/256] refactor(api/internal/monkeypatch.js): patch global 'Notification' --- api/internal/monkeypatch.js | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/api/internal/monkeypatch.js b/api/internal/monkeypatch.js index b7a57fb0b0..40f0f17419 100644 --- a/api/internal/monkeypatch.js +++ b/api/internal/monkeypatch.js @@ -1,6 +1,7 @@ /* global MutationObserver */ import { fetch, Headers, Request, Response } from '../fetch.js' import { URL, URLPattern, URLSearchParams } from '../url.js' +import Notification from '../notification.js' import geolocation from './geolocation.js' import permissions from './permissions.js' @@ -28,16 +29,30 @@ export function init () { fetch, Headers, Request, - Response + Response, + + // notifications + Notification }) try { - globalThis.navigator.geolocation = Object.assign(globalThis.navigator?.geolocation ?? {}, geolocation) + // @ts-ignore + globalThis.navigator.geolocation = Object.assign( + globalThis.navigator?.geolocation ?? {}, + geolocation + ) } catch {} - try { - globalThis.navigator.permissions = Object.assign(globalThis.navigator?.permissions ?? {}, permissions) - } catch {} + if (!globalThis.navigator?.permissions) { + // @ts-ignore + globalThis.navigator.permissions = permissions + } else { + try { + for (const key in permissions) { + globalThis.navigator.permissions[key] = permissions[key] + } + } catch {} + } applied = true // create <title> tag in document if it doesn't exist From 01ced472da0e26d968cf8c848fb9df7ae06a0609 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:44:49 -0400 Subject: [PATCH 122/256] refactor(api/hooks.js): export module functions, clean up, handle notification events --- api/hooks.js | 178 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 174 insertions(+), 4 deletions(-) diff --git a/api/hooks.js b/api/hooks.js index 4a8081760d..bd73bb217d 100644 --- a/api/hooks.js +++ b/api/hooks.js @@ -41,6 +41,22 @@ * hooks.onOffline((event) => { * // called when 'offline' events are dispatched on the global object * }) + * + * hooks.onLanguageChange((event) => { + * // called when 'languagechange' events are dispatched on the global object + * }) + * + * hooks.onPermissionChange((event) => { + * // called when 'permissionchange' events are dispatched on the global object + * }) + * + * hooks.onNotificationResponse((event) => { + * // called when 'notificationresponse' events are dispatched on the global object + * }) + * + * hooks.onNotificationPresented((event) => { + * // called when 'notificationpresented' events are dispatched on the global object + * }) * ``` */ import { Event, CustomEvent, ErrorEvent, MessageEvent } from './events.js' @@ -74,13 +90,17 @@ function dispatchReadyEvent (target) { function proxyGlobalEvents (global, target) { const GLOBAL_EVENTS = [ 'data', + 'error', 'init', + 'languagechange', 'load', 'message', - 'online', - 'offline', - 'error', 'messageerror', + 'notificationpresented', + 'notificationresponse', + 'offline', + 'online', + 'permissionchange', 'unhandledrejection' ] @@ -378,6 +398,156 @@ export class Hooks extends EventTarget { this.addEventListener('languagechange', callback) return () => this.removeEventListener('languagechange', callback) } + + /** + * Calls callback when an application permission has changed. + * @param {function} callback + * @return {function} + */ + onPermissionChange (callback) { + this.addEventListener('permissionchange', callback) + return () => this.removeEventListener('permissionchange', callback) + } + + /** + * Calls callback in response to a displayed `Notification`. + * @param {function} callback + * @return {function} + */ + onNotificationResponse (callback) { + this.addEventListener('notificationresponse', callback) + return () => this.removeEventListener('notificationresponse', callback) + } + + /** + * Calls callback when a `Notification` is presented. + * @param {function} callback + * @return {function} + */ + onNotificationPresented (callback) { + this.addEventListener('notificationpresented', callback) + return () => this.removeEventListener('notificationpresented', callback) + } +} + +/** + * `Hooks` single instance. + * @ignore + */ +const hooks = new Hooks() + +/** + * Wait for the global Window, Document, and Runtime to be ready. + * The callback function is called exactly once. + * @param {function} callback + * @return {function} + */ +export function onReady (callback) { + return hooks.onReady(callback) +} + +/** + * Wait for the global Window and Document to be ready. The callback + * function is called exactly once. + * @param {function} callback + * @return {function} + */ +export function onLoad (callback) { + return hooks.onLoad(callback) +} + +/** + * Wait for the runtime to be ready. The callback + * function is called exactly once. + * @param {function} callback + * @return {function} + */ +export function onInit (callback) { + return hooks.onInit(callback) +} + +/** + * Calls callback when a global exception occurs. + * 'error', 'messageerror', and 'unhandledrejection' events are handled here. + * @param {function} callback + * @return {function} + */ +export function onError (callback) { + return hooks.onError(callback) +} + +/** + * Subscribes to the global data pipe calling callback when + * new data is emitted on the global Window. + * @param {function} callback + * @return {function} + */ +export function onData (callback) { + return hooks.onData(callback) +} + +/** + * Subscribes to global messages likely from an external `postMessage` + * invocation. + * @param {function} callback + * @return {function} + */ +export function onMessage (callback) { + return hooks.onMessage(callback) +} + +/** + * Calls callback when runtime is working online. + * @param {function} callback + * @return {function} + */ +export function onOnline (callback) { + return hooks.onOnline(callback) +} + +/** + * Calls callback when runtime is not working online. + * @param {function} callback + * @return {function} + */ +export function onOffline (callback) { + return hooks.onOffline(callback) +} + +/** + * Calls callback when runtime user preferred language has changed. + * @param {function} callback + * @return {function} + */ +export function onLanguageChange (callback) { + return hooks.onLanguageChange(callback) +} + +/** + * Calls callback when an application permission has changed. + * @param {function} callback + * @return {function} + */ +export function onPermissionChange (callback) { + return hooks.onPermissionChange(callback) +} + +/** + * Calls callback in response to a presented `Notification`. + * @param {function} callback + * @return {function} + */ +export function onNotificationResponse (callback) { + return hooks.onNotificationResponse(callback) +} + +/** + * Calls callback when a `Notification` is presented. + * @param {function} callback + * @return {function} + */ +export function onNotificationPresented (callback) { + return hooks.onNotificationPresented(callback) } -export default new Hooks() +export default hooks From 53de68b923a6a9f684aea72c34c3f36d3f79b393 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 13:45:13 -0400 Subject: [PATCH 123/256] chore(api); generate docs/types --- api/CONFIG.md | 5 +- api/index.d.ts | 563 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 539 insertions(+), 29 deletions(-) diff --git a/api/CONFIG.md b/api/CONFIG.md index 27d453d867..62453afe7a 100644 --- a/api/CONFIG.md +++ b/api/CONFIG.md @@ -134,9 +134,8 @@ appstore_icon | | Mac App Store icon category | | A category in the App Store cmd | | The command to execute to spawn the "back-end" process. icon | | The icon to use for identifying your app on MacOS. -sign | | TODO Signing guide: https://socketsupply.co/guides/#code-signing-certificates -codesign_identity | | -sign_paths | | +codesign_identity | | TODO Signing guide: https://socketsupply.co/guides/#code-signing-certificates +codesign_paths | | Additional paths to codesign # Section `native` diff --git a/api/index.d.ts b/api/index.d.ts index bb227d0d07..cb049a9b51 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -4228,6 +4228,84 @@ declare module "socket:fetch" { import fetch from "socket:fetch/index"; } declare module "socket:hooks" { + /** + * Wait for the global Window, Document, and Runtime to be ready. + * The callback function is called exactly once. + * @param {function} callback + * @return {function} + */ + export function onReady(callback: Function): Function; + /** + * Wait for the global Window and Document to be ready. The callback + * function is called exactly once. + * @param {function} callback + * @return {function} + */ + export function onLoad(callback: Function): Function; + /** + * Wait for the runtime to be ready. The callback + * function is called exactly once. + * @param {function} callback + * @return {function} + */ + export function onInit(callback: Function): Function; + /** + * Calls callback when a global exception occurs. + * 'error', 'messageerror', and 'unhandledrejection' events are handled here. + * @param {function} callback + * @return {function} + */ + export function onError(callback: Function): Function; + /** + * Subscribes to the global data pipe calling callback when + * new data is emitted on the global Window. + * @param {function} callback + * @return {function} + */ + export function onData(callback: Function): Function; + /** + * Subscribes to global messages likely from an external `postMessage` + * invocation. + * @param {function} callback + * @return {function} + */ + export function onMessage(callback: Function): Function; + /** + * Calls callback when runtime is working online. + * @param {function} callback + * @return {function} + */ + export function onOnline(callback: Function): Function; + /** + * Calls callback when runtime is not working online. + * @param {function} callback + * @return {function} + */ + export function onOffline(callback: Function): Function; + /** + * Calls callback when runtime user preferred language has changed. + * @param {function} callback + * @return {function} + */ + export function onLanguageChange(callback: Function): Function; + /** + * Calls callback when an application permission has changed. + * @param {function} callback + * @return {function} + */ + export function onPermissionChange(callback: Function): Function; + /** + * Calls callback in response to a presented `Notification`. + * @param {function} callback + * @return {function} + */ + export function onNotificationResponse(callback: Function): Function; + /** + * Calls callback when a `Notification` is presented. + * @param {function} callback + * @return {function} + */ + export function onNotificationPresented(callback: Function): Function; /** * An event dispatched when the runtime has been initialized. */ @@ -4361,10 +4439,32 @@ declare module "socket:hooks" { * @return {function} */ onLanguageChange(callback: Function): Function; + /** + * Calls callback when an application permission has changed. + * @param {function} callback + * @return {function} + */ + onPermissionChange(callback: Function): Function; + /** + * Calls callback in response to a displayed `Notification`. + * @param {function} callback + * @return {function} + */ + onNotificationResponse(callback: Function): Function; + /** + * Calls callback when a `Notification` is presented. + * @param {function} callback + * @return {function} + */ + onNotificationPresented(callback: Function): Function; #private; } - const _default: Hooks; - export default _default; + export default hooks; + /** + * `Hooks` single instance. + * @ignore + */ + const hooks: Hooks; } declare module "socket:language" { /** @@ -6381,6 +6481,427 @@ declare module "socket:node-esm-loader" { export function resolve(specifier: any, ctx: any, next: any): Promise<any>; export default resolve; } +declare module "socket:internal/permissions" { + /** + * Query for a permission status. + * @param {PermissionDescriptor} descriptor + * @param {object=} [options] + * @param {?AbortSignal} [options.signal = null] + * @return {Promise<PermissionStatus>} + */ + export function query(descriptor: PermissionDescriptor, options?: object | undefined, ...args: any[]): Promise<PermissionStatus>; + /** + * Request a permission to be granted. + * @param {PermissionDescriptor} descriptor + * @param {object=} [options] + * @param {?AbortSignal} [options.signal = null] + * @return {Promise<PermissionStatus>} + */ + export function request(descriptor: PermissionDescriptor, options?: object | undefined, ...args: any[]): Promise<PermissionStatus>; + /** + * An enumeration of the permission types. + * - 'geolocation' + * - 'notifications' + * - 'push' + * - 'persistent-storage' + * - 'midi' + * - 'storage-access' + * @type {Enumeration} + * @ignore + */ + export const types: Enumeration; + const _default: any; + export default _default; + export type PermissionDescriptor = { + name: string; + }; + /** + * A container that provides the state of an object and an event handler + * for monitoring changes permission changes. + * @ignore + */ + class PermissionStatus extends EventTarget { + /** + * `PermissionStatus` class constructor. + * @param {string} name + * @param {string} initialState + * @param {object=} [options] + * @param {?AbortSignal} [options.signal = null] + */ + constructor(name: string, initialState: string, options?: object | undefined); + /** + * The name of this permission this status is for. + * @type {string} + */ + get name(): string; + /** + * The current state of the permission status. + * @type {string} + */ + get state(): string; + set onchange(arg: (arg0: Event) => any); + /** + * Level 0 event target 'change' event listener accessor + * @type {function(Event)} + */ + get onchange(): (arg0: Event) => any; + /** + * Non-standard method for unsubscribing to status state updates. + * @ignore + */ + unsubscribe(): void; + /** + * String tag for `PermissionStatus`. + * @ignore + */ + get [Symbol.toStringTag](): string; + #private; + } + import Enumeration from "socket:enumeration"; +} +declare module "socket:notification" { + /** + * Show a notification. Creates a `Notification` instance and displays + * it to the user. + * @param {string} title + * @param {NotificationOptions=} [options] + * @param {function(Event)=} [onclick] + * @param {function(Event)=} [onclose] + * @return {Promise} + */ + export function showNotification(title: string, options?: NotificationOptions | undefined, onclick?: ((arg0: Event) => any) | undefined, onshow?: any): Promise<any>; + /** + * An enumeratino of notification test directions: + * - 'auto' Automatically determined by the operating system + * - 'ltr' Left-to-right text direction + * - 'rtl' Right-to-left text direction + * @type {Enumeration} + * @ignore + */ + export const NotificationDirection: Enumeration; + /** + * An enumeration of permission types granted by the user for the current + * origin to display notifications to the end user. + * - 'granted' The user has explicitly granted permission for the current + * origin to display system notifications. + * - 'denied' The user has explicitly denied permission for the current + * origin to display system notifications. + * - 'default' The user decision is unknown; in this case the application + * will act as if permission was denied. + * @type {Enumeration} + * @ignore + */ + export const NotificationPermission: Enumeration; + /** + * A validated notification action object container. + * You should never need to construct this. + * @ignore + */ + export class NotificationAction { + /** + * `NotificationAction` class constructor. + * @ignore + * @param {object} options + * @param {string} options.action + * @param {string} options.title + * @param {string|URL=} [options.icon = ''] + */ + constructor(options: { + action: string; + title: string; + icon?: (string | URL) | undefined; + }); + /** + * A string identifying a user action to be displayed on the notification. + * @type {string} + */ + get action(): string; + /** + * A string containing action text to be shown to the user. + * @type {string} + */ + get title(): string; + /** + * A string containing the URL of an icon to display with the action. + * @type {string} + */ + get icon(): string; + #private; + } + /** + * A validated notification options object container. + * You should never need to construct this. + * @ignore + */ + export class NotificationOptions { + /** + * `NotificationOptions` class constructor. + * @ignore + * @param {object} [options = {}] + * @param {'auto'|'ltr|'rtl'=} [options.dir = 'auto'] + * @param {NotificationAction[]=} [options.actions = []] + * @param {string|URL=} [options.badge = ''] + * @param {string=} [options.body = ''] + * @param {?any=} [options.data = null] + * @param {string|URL=} [options.icon = ''] + * @param {string|URL=} [options.image = ''] + * @param {string=} [options.lang = ''] + * @param {string=} [options.tag = ''] + * @param {boolean=} [options.boolean = ''] + * @param {boolean=} [options.requireInteraction = false] + * @param {boolean=} [options.silent = false] + * @param {number[]=} [options.vibrate = []] + */ + constructor(options?: object); + /** + * An array of actions to display in the notification. + * @type {NotificationAction[]} + */ + get actions(): NotificationAction[]; + /** + * A string containing the URL of the image used to represent + * the notification when there isn't enough space to display the + * notification itself. + * @type {string} + */ + get badge(): string; + /** + * A string representing the body text of the notification, + * which is displayed below the title. + * @type {string} + */ + get body(): string; + /** + * Arbitrary data that you want associated with the notification. + * This can be of any data type. + * @type {?any} + */ + get data(): any; + /** + * The direction in which to display the notification. + * It defaults to 'auto', which just adopts the environments + * language setting behavior, but you can override that behavior + * by setting values of 'ltr' and 'rtl'. + * @type {'auto'|'ltr'|'rtl'} + */ + get dir(): "auto" | "ltr" | "rtl"; + /** + * A string containing the URL of an icon to be displayed in the notification. + * @type {string} + */ + get icon(): string; + /** + * The URL of an image to be displayed as part of the notification, as + * specified in the constructor's options parameter. + * @type {string} + */ + get image(): string; + /** + * The notification's language, as specified using a string representing a + * language tag according to RFC 5646. + * @type {string} + */ + get lang(): string; + /** + * A boolean value specifying whether the user should be notified after a + * new notification replaces an old one. The default is `false`, which means + * they won't be notified. If `true`, then tag also must be set. + * @type {boolean} + */ + get renotify(): boolean; + /** + * Indicates that a notification should remain active until the user clicks + * or dismisses it, rather than closing automatically. + * The default value is `false`. + * @type {boolean} + */ + get requireInteraction(): boolean; + /** + * A boolean value specifying whether the notification is silent (no sounds + * or vibrations issued), regardless of the device settings. + * The default is `false`, which means it won't be silent. If `true`, then + * vibrate must not be present. + * @type {boolean} + */ + get silent(): boolean; + /** + * A string representing an identifying tag for the notification. + * The default is the empty string. + * @type {string} + */ + get tag(): string; + /** + * A vibration pattern for the device's vibration hardware to emit with + * the notification. If specified, silent must not be `true`. + * @type {number[]} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API#vibration_patterns} + */ + get vibrate(): number[]; + #private; + } + /** + * The Notification interface is used to configure and display + * desktop and mobile notifications to the user. + */ + export class Notification extends EventTarget { + /** + * A read-only property that indicates the current permission granted + * by the user to display notifications. + * @type {'prompt'|'granted'|'denied'} + */ + static get permission(): "denied" | "granted" | "prompt"; + /** + * The maximum number of actions supported by the device. + * @type {number} + */ + static get maxActions(): number; + /** + * Requests permission from the user to display notifications. + */ + static requestPermission(): Promise<any>; + /** + * `Notification` class constructor. + * @param {string} title + * @param {NotificationOptions=} [options] + */ + constructor(title: string, options?: NotificationOptions | undefined, ...args: any[]); + /** + * A unique identifier for this notification. + * @type {string} + */ + get id(): string; + set onclick(arg: Function); + /** + * The click event is dispatched when the user clicks on + * displayed notification. + * @type {?function} + */ + get onclick(): Function; + set onclose(arg: Function); + /** + * The close event is dispatched when the notification closes. + * @type {?function} + */ + get onclose(): Function; + set onerror(arg: Function); + /** + * The eror event is dispatched when the notification fails to display + * or encounters an error. + * @type {?function} + */ + get onerror(): Function; + set onshow(arg: Function); + /** + * The click event is dispatched when the notification is displayed. + * @type {?function} + */ + get onshow(): Function; + /** + * An array of actions to display in the notification. + * @type {NotificationAction[]} + */ + get actions(): NotificationAction[]; + /** + * A string containing the URL of the image used to represent + * the notification when there isn't enough space to display the + * notification itself. + * @type {string} + */ + get badge(): string; + /** + * A string representing the body text of the notification, + * which is displayed below the title. + * @type {string} + */ + get body(): string; + /** + * Arbitrary data that you want associated with the notification. + * This can be of any data type. + * @type {?any} + */ + get data(): any; + /** + * The direction in which to display the notification. + * It defaults to 'auto', which just adopts the environments + * language setting behavior, but you can override that behavior + * by setting values of 'ltr' and 'rtl'. + * @type {'auto'|'ltr'|'rtl'} + */ + get dir(): "auto" | "ltr" | "rtl"; + /** + * A string containing the URL of an icon to be displayed in the notification. + * @type {string} + */ + get icon(): string; + /** + * The URL of an image to be displayed as part of the notification, as + * specified in the constructor's options parameter. + * @type {string} + */ + get image(): string; + /** + * The notification's language, as specified using a string representing a + * language tag according to RFC 5646. + * @type {string} + */ + get lang(): string; + /** + * A boolean value specifying whether the user should be notified after a + * new notification replaces an old one. The default is `false`, which means + * they won't be notified. If `true`, then tag also must be set. + * @type {boolean} + */ + get renotify(): boolean; + /** + * Indicates that a notification should remain active until the user clicks + * or dismisses it, rather than closing automatically. + * The default value is `false`. + * @type {boolean} + */ + get requireInteraction(): boolean; + /** + * A boolean value specifying whether the notification is silent (no sounds + * or vibrations issued), regardless of the device settings. + * The default is `false`, which means it won't be silent. If `true`, then + * vibrate must not be present. + * @type {boolean} + */ + get silent(): boolean; + /** + * A string representing an identifying tag for the notification. + * The default is the empty string. + * @type {string} + */ + get tag(): string; + /** + * A vibration pattern for the device's vibration hardware to emit with + * the notification. If specified, silent must not be `true`. + * @type {number[]} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Vibration_API#vibration_patterns} + */ + get vibrate(): number[]; + /** + * The timestamp of the notification. + * @type {number} + */ + get timestamp(): number; + /** + * The title read-only property of the `Notification` instace indicates + * the title of the notification, as specified in the `title` parameter + * of the `Notification` constructor. + * @type {string} + */ + get title(): string; + /** + * Closes the notification programmatically. + */ + close(): Promise<void>; + #private; + } + export default Notification; + import { Enumeration } from "socket:enumeration"; + import URL from "socket:url"; +} declare module "socket:stream-relay" { export * from "socket:stream-relay/index"; export default def; @@ -6388,6 +6909,7 @@ declare module "socket:stream-relay" { } declare module "socket:internal/geolocation" { /** + * Get the current position of the device. * @param {function(GeolocationPosition)} onSuccess * @param {onError(Error)} onError * @param {object=} options @@ -6396,14 +6918,22 @@ declare module "socket:internal/geolocation" { */ export function getCurrentPosition(onSuccess: (arg0: GeolocationPosition) => any, onError: any, options?: object | undefined, ...args: any[]): Promise<any>; /** + * Register a handler function that will be called automatically each time the + * position of the device changes. You can also, optionally, specify an error + * handling callback function. * @param {function(GeolocationPosition)} onSuccess * @param {function(Error)} onError - * @param {object=} options - * @param {number=} options.timeout - * @return {Promise} + * @param {object=} [options] + * @param {number=} [options.timeout = null] + * @return {number} + */ + export function watchPosition(onSuccess: (arg0: GeolocationPosition) => any, onError: (arg0: Error) => any, options?: object | undefined, ...args: any[]): number; + /** + * Unregister location and error monitoring handlers previously installed + * using `watchPosition`. + * @param {number} id */ - export function watchPosition(onSuccess: (arg0: GeolocationPosition) => any, onError: (arg0: Error) => any, options?: object | undefined, ...args: any[]): Promise<any>; - export function clearWatch(id: any, ...args: any[]): any; + export function clearWatch(id: number, ...args: any[]): any; export namespace platform { let getCurrentPosition: Function; let watchPosition: Function; @@ -6434,25 +6964,6 @@ declare module "socket:internal/globals" { get(name: any): any; }; } -declare module "socket:internal/permissions" { - /** - * @param {{ name: string }} descriptor - * @return {Promise<PermissionStatus>} - */ - export function query(descriptor: { - name: string; - }, ...args: any[]): Promise<PermissionStatus>; - const _default: any; - export default _default; - class PermissionStatus extends EventTarget { - constructor(name: any, subscribe: any); - get name(): string; - get state(): any; - set onchange(arg: any); - get onchange(): any; - #private; - } -} declare module "socket:internal/monkeypatch" { export function init(): void; const _default: void; From 18ea4d702010ed1830d778b2ae6a027d1d2d358e Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 14:27:42 -0400 Subject: [PATCH 124/256] refactor(src/android/main.kt): emit 'permissionchange' events --- src/android/main.kt | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/android/main.kt b/src/android/main.kt index 8aa5fa8577..2889d309d7 100644 --- a/src/android/main.kt +++ b/src/android/main.kt @@ -200,9 +200,49 @@ open class MainActivity : WebViewActivity() { for (request in this.permissionRequests) { if (request.id == requestCode) { this.permissionRequests.remove(request) - request.callback(grantResults.all { r -> r == android.content.pm.PackageManager.PERMISSION_GRANTED }) + request.callback(grantResults.all { r -> + r == android.content.pm.PackageManager.PERMISSION_GRANTED + }) break } } + + var i = 0 + val seen = mutableSetOf<String>() + for (permission in permissions) { + val granted = ( + grantResults[i++] == android.content.pm.PackageManager.PERMISSION_GRANTED + ) + + var name = "" + when (permission) { + "android.permission.ACCESS_COARSE_LOCATION", + "android.permission.ACCESS_FINE_LOCATION" -> { + name = "geolocation" + } + + "android.permission.POST_NOTIFICATIONS" -> { + name = "notifications" + } + } + + if (seen.contains(name)) { + continue + } + + if (name.length == 0) { + continue + } + + seen.add(name) + + this.runOnUiThread { + val state = if (granted) "grated" else "denied" + window.bridge.emit("permissionchange", """{ + "name": "$name", + "state": "$state" + }""") + } + } } } From 36b5930b78cddffefd60a76544494cfe1dbd343c Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 14:51:01 -0400 Subject: [PATCH 125/256] refactor(api/internal/permissions.js): dispatch global event upon request success --- api/internal/permissions.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/api/internal/permissions.js b/api/internal/permissions.js index e6c91bb96c..d149e6672d 100644 --- a/api/internal/permissions.js +++ b/api/internal/permissions.js @@ -1,4 +1,4 @@ -/* global EventTarget, Event */ +/* global EventTarget, CustomEvent, Event */ /** * @module Permissions * This module provides an API for querying and requesting permissions. @@ -275,6 +275,17 @@ export async function request (descriptor, options) { throw result.err } + const globalEvent = new CustomEvent('permissionchange', { + detail: { + name, + state: result.data.state + } + }) + + queueMicrotask(() => { + globalThis.dispatchEvent(globalEvent) + }) + return new PermissionStatus(name, result.data.state, options) } From 234c36cc3a6098261503109b185c3941290fd21e Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 22:31:57 +0200 Subject: [PATCH 126/256] fix(api/ipc.js): handle undefined 'value' in IPC options --- api/ipc.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/api/ipc.js b/api/ipc.js index 44728842f1..4f381d3a94 100644 --- a/api/ipc.js +++ b/api/ipc.js @@ -993,7 +993,7 @@ export async function ready () { if (Date.now() - startReady > 10000) { reject(new Error('failed to resolve globalThis.__args')) } else if (globalThis.__args) { - queueMicrotask(resolve) + queueMicrotask(() => resolve()) } else { queueMicrotask(loop) } @@ -1010,14 +1010,17 @@ class IPCSearchParams extends URLSearchParams { value = params params = null } + super({ - index: globalThis.__args?.index ?? 0, ...params, + index: globalThis.__args?.index ?? 0, seq: 'R' + nextSeq++ }) + if (value !== undefined) { this.set('value', value) } + if (nonce) { this.set('nonce', nonce) } From 60356ae78a060fa3543644174b4628b910d3f7c4 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 10 Oct 2023 22:32:52 +0200 Subject: [PATCH 127/256] chore(api/README.md): generate docs --- api/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/README.md b/api/README.md index bcf344c107..563b290c5b 100644 --- a/api/README.md +++ b/api/README.md @@ -1107,7 +1107,7 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesw import { send } from 'socket:ipc' ``` -## [`emit(name, value, target, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1082) +## [`emit(name, value, target, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1089) Emit event to be dispatched on `window` object. @@ -1118,7 +1118,7 @@ Emit event to be dispatched on `window` object. | target | EventTarget | window | true | | | options | Object | | true | | -## [`send(command, value, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1141) +## [`send(command, value, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1148) Sends an async IPC command request with parameters. From 55ecaed20439f904965bb82366c7d1c96a4f7e27 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 08:16:55 -0400 Subject: [PATCH 128/256] chore(bin/install.sh): bump 'WebView2' version --- bin/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/install.sh b/bin/install.sh index 184a6d4398..874868cea9 100755 --- a/bin/install.sh +++ b/bin/install.sh @@ -354,7 +354,7 @@ function _get_web_view2() { echo "# Downloading Webview2" - curl -L https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/1.0.1901.177 --output "$tmp/webview2.zip" + curl -L https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/1.0.2045.177 --output "$tmp/webview2.zip" cd "$tmp" || exit 1 unzip -q "$tmp/webview2.zip" mkdir -p "$BUILD_DIR/include" From 8cc793f69611b7ee04fa6622202e998433b0efff Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 08:26:03 -0400 Subject: [PATCH 129/256] chore(api/README.md): generate docs --- api/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/README.md b/api/README.md index 563b290c5b..73abc4c2fa 100644 --- a/api/README.md +++ b/api/README.md @@ -1107,7 +1107,7 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesw import { send } from 'socket:ipc' ``` -## [`emit(name, value, target, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1089) +## [`emit(name, value, target, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1087) Emit event to be dispatched on `window` object. @@ -1118,7 +1118,7 @@ Emit event to be dispatched on `window` object. | target | EventTarget | window | true | | | options | Object | | true | | -## [`send(command, value, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1148) +## [`send(command, value, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1146) Sends an async IPC command request with parameters. From e2edb962f4e5e40a1c52f82e0e5fac26d5533943 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 14:40:20 +0200 Subject: [PATCH 130/256] refactor(linux): add missing linux implementation --- api/internal/monkeypatch.js | 72 ++++++--- api/internal/permissions.js | 24 ++- api/notification.js | 162 +++++++++++++++++++-- src/window/linux.cc | 26 ++++ test/scripts/bootstrap-android-emulator.sh | 2 +- 5 files changed, 250 insertions(+), 36 deletions(-) diff --git a/api/internal/monkeypatch.js b/api/internal/monkeypatch.js index 40f0f17419..d7a386ba57 100644 --- a/api/internal/monkeypatch.js +++ b/api/internal/monkeypatch.js @@ -12,6 +12,55 @@ let applied = false export function init () { if (applied || !globalThis.window) return + function install (name, implementation, target = globalThis, prefix) { + if (typeof name === 'object') { + for (const key in name) { + install(key, name[key], target || implementation, prefix || target) + } + return + } + + const actualName = name.split('.').slice(-1)[0] + + if (typeof prefix === 'string') { + name = `${prefix}.${name}` + } + + if (typeof target[actualName] === 'object' && target[actualName] !== null) { + for (const key in implementation) { + const nativeImplementation = target[actualName][key] || null + // let this fail, the environment implementation may not be writable + try { + target[actualName][key] = implementation[key] + } catch {} + + if (nativeImplementation !== null) { + const nativeName = ['_', 'native', ...name.split('.'), key].join('_') + Object.defineProperty(globalThis, nativeName, { + enumerable: false, + configurable: false, + value: nativeImplementation + }) + } + } + } else { + const nativeImplementation = target[actualName] || null + // let this fail, the environment implementation may not be writable + try { + target[actualName] = implementation + } catch {} + + if (nativeImplementation !== null) { + const nativeName = ['_', 'native', ...name.split('.')].join('_') + Object.defineProperty(globalThis, nativeName, { + enumerable: false, + configurable: false, + value: nativeImplementation + }) + } + } + } + if ( typeof globalThis.webkitSpeechRecognition === 'function' && typeof globalThis.SpeechRecognition !== 'function' @@ -19,7 +68,8 @@ export function init () { globalThis.SpeechRecognition = globalThis.webkitSpeechRecognition } - Object.assign(globalThis, { + // globals + install({ // url URL, URLPattern, @@ -35,24 +85,8 @@ export function init () { Notification }) - try { - // @ts-ignore - globalThis.navigator.geolocation = Object.assign( - globalThis.navigator?.geolocation ?? {}, - geolocation - ) - } catch {} - - if (!globalThis.navigator?.permissions) { - // @ts-ignore - globalThis.navigator.permissions = permissions - } else { - try { - for (const key in permissions) { - globalThis.navigator.permissions[key] = permissions[key] - } - } catch {} - } + // navigator + install({ geolocation, permissions }, globalThis.navigator, 'navigator') applied = true // create <title> tag in document if it doesn't exist diff --git a/api/internal/permissions.js b/api/internal/permissions.js index d149e6672d..aa0808f69c 100644 --- a/api/internal/permissions.js +++ b/api/internal/permissions.js @@ -4,6 +4,7 @@ * This module provides an API for querying and requesting permissions. */ import { IllegalConstructorError } from '../errors.js' +import Notification from '../notification.js' import Enumeration from '../enumeration.js' import hooks from '../hooks.js' import ipc from '../ipc.js' @@ -16,6 +17,7 @@ import os from '../os.js' const isAndroid = os.platform() === 'android' const isApple = os.platform() === 'darwin' +const isLinux = os.platform() === 'linux' /** * Get a bound platform `navigator.permissions` function. @@ -169,7 +171,6 @@ class PermissionStatus extends EventTarget { return { args: [this.removePermissionChangeListener], handle (removePermissionChangeListener) { - console.log('GCd') removePermissionChangeListener() } } @@ -269,6 +270,27 @@ export async function request (descriptor, options) { ) } + if (isLinux) { + if (name === 'notifications') { + const currentState = Notification.permission + const state = await Notification.requestPermission() + + if (currentState !== state) { + const globalEvent = new CustomEvent('permissionchange', { + detail: { name, state } + }) + + queueMicrotask(() => { + globalThis.dispatchEvent(globalEvent) + }) + } + + return new PermissionStatus(name, state, options) + } + + return new PermissionStatus(name, 'prompt', options) + } + const result = await ipc.send('permissions.request', { name, signal: options?.signal }) if (result.err) { diff --git a/api/notification.js b/api/notification.js index 0eaae4501c..f66b30a52a 100644 --- a/api/notification.js +++ b/api/notification.js @@ -1,4 +1,4 @@ -/* global Event, ErrorEvent, EventTarget */ +/* global CustomEvent, Event, ErrorEvent, EventTarget */ /** * @module Notification * The Notification modules provides an API to configure and display @@ -13,6 +13,10 @@ import location from './location.js' import hooks from './hooks.js' import URL from './url.js' import ipc from './ipc.js' +import os from './os.js' + +const isLinux = os.platform() === 'linux' +const NativeNotification = globalThis.Notification /** * Used to determine if notification beign created in a `ServiceWorker`. @@ -27,6 +31,104 @@ const isServiceWorkerGlobalScope = typeof globalThis.registration?.active === 's */ const DEFAULT_MAX_ACTIONS = 2 +/** + * The global event dispatched when a `Notification` is presented to + * the user. + * @ignore + * @type {string} + */ +export const NOTIFICATION_PRESENTED_EVENT = 'notificationpresented' + +/** + * The global event dispatched when a `Notification` has a response + * from the user. + * @ignore + * @type {string} + */ +export const NOTIFICATION_RESPONSE_EVENT = 'notificationresponse' + +/** + * A container to proxy native notification events to a runtime notication. + * @ignore + */ +class NativeNotificationProxy extends NativeNotification { + /** + * @type {Notification} + * @ignore + */ + #notification = null + + /** + * `NativeNotificationProxy` class constructor. + * @param {Notification} notification + * @ignore + */ + constructor (notification) { + super(notification.title, notification) + + let clicked = false + let error = false + + this.#notification = notification + + this.onerror = (event) => { + error = true + notification.dispatchEvent(new ErrorEvent('error', { + error: ( + event.error || + event.message || + new Error('An unknown error occured', { cause: event }) + ) + })) + } + + this.onclose = (event) => { + if (error) return + if (!clicked) { + const event = new CustomEvent(NOTIFICATION_RESPONSE_EVENT, { + detail: { + id: notification.id, + action: 'dismiss' + } + }) + + globalThis.dispatchEvent(event) + } + } + + this.onclick = () => { + if (error) return + clicked = true + const event = new CustomEvent(NOTIFICATION_RESPONSE_EVENT, { + detail: { + id: notification.id, + action: 'default' + } + }) + + globalThis.dispatchEvent(event) + } + + this.onshow = () => { + if (error) return + const event = new CustomEvent(NOTIFICATION_PRESENTED_EVENT, { + detail: { id: notification.id } + }) + + globalThis.dispatchEvent(event) + } + } + + /** + * The underlying `Notification` this proxy dispatches events to. + * @type {Notification} + * @ignore + */ + get notification () { + return this.#notification + } +} + /** * An enumeratino of notification test directions: * - 'auto' Automatically determined by the operating system @@ -422,8 +524,17 @@ export class Notification extends EventTarget { /** * Requests permission from the user to display notifications. + * @return {Promise<'granted'|'default'|'denied'>} */ static async requestPermission () { + if (isLinux) { + if (typeof NativeNotification?.requestPermission === 'function') { + return await NativeNotification.requestPermission() + } + + return 'denied' + } + const status = await permissions.request({ name: 'notifications' }) status.unsubscribe() return status.state @@ -439,6 +550,8 @@ export class Notification extends EventTarget { #title = null #id = null + #proxy = null + /** * `Notification` class constructor. * @param {string} title @@ -477,18 +590,29 @@ export class Notification extends EventTarget { this.#id = (rand64() & 0xFFFFn).toString() - const request = ipc.request('notification.show', { - body: this.body, - icon: this.icon, - id: this.#id, - image: this.image, - lang: this.lang, - tag: this.tag || '', - title: this.title, - silent: this.silent - }) - - this[Symbol.for('Notification.request')] = request + if (isLinux) { + const proxy = new NativeNotificationProxy(this) + const request = new Promise((resolve) => { + proxy.addEventListener('show', () => resolve({})) + proxy.addEventListener('error', (e) => resolve({ err: e.error })) + }) + + this.#proxy = proxy + this[Symbol.for('Notification.request')] = request + } else { + const request = ipc.request('notification.show', { + body: this.body, + icon: this.icon, + id: this.#id, + image: this.image, + lang: this.lang, + tag: this.tag || '', + title: this.title, + silent: this.silent + }) + + this[Symbol.for('Notification.request')] = request + } state.pending.set(this.id, this) @@ -511,8 +635,8 @@ export class Notification extends EventTarget { }) // propagate error to caller - request.then((result) => { - if (result.err) { + this[Symbol.for('Notification.request')].then((result) => { + if (result?.err) { state.pending.delete(this.id, this) removeNotificationPresentedListener() removeNotificationResponseListener() @@ -733,6 +857,14 @@ export class Notification extends EventTarget { * Closes the notification programmatically. */ async close () { + if (isLinux) { + if (this.#proxy) { + return this.#proxy.close() + } + + return + } + const result = await ipc.request('notification.close', { id: this.id }) if (result.err) { console.warn('Failed to close \'Notification\': %s', result.err.message) diff --git a/src/window/linux.cc b/src/window/linux.cc index b83370e041..ef9634dd26 100644 --- a/src/window/linux.cc +++ b/src/window/linux.cc @@ -204,6 +204,32 @@ namespace SSC { webkit_cookie_manager_set_accept_policy(cookieManager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS); + g_signal_connect( + G_OBJECT(webview), + "show-notification", + G_CALLBACK(+[]( + WebKitWebView* webview, + WebKitNotification* notification, + gpointer userData + ) -> bool { + return false; + }), + this + ); + + g_signal_connect( + G_OBJECT(webview), + "close-notification", + G_CALLBACK(+[]( + WebKitWebView* webview, + WebKitNotification* notification, + gpointer userData + ) -> bool { + return false; + }), + this + ); + g_signal_connect( G_OBJECT(webview), "permission-request", diff --git a/test/scripts/bootstrap-android-emulator.sh b/test/scripts/bootstrap-android-emulator.sh index 8d58fdbe98..6ce2e3a739 100755 --- a/test/scripts/bootstrap-android-emulator.sh +++ b/test/scripts/bootstrap-android-emulator.sh @@ -94,7 +94,7 @@ write_code 0 if ! "$avdmanager" list avd | grep 'Name: SSCAVD$'; then echo "Downloading AVD image..." - pkg="system-images;android-33;google_apis;$(uname -m | sed -E 's/(arm64|aarch64)/arm64-v8a/g')" + pkg="system-images;android-34;google_apis;$(uname -m | sed -E 's/(arm64|aarch64)/arm64-v8a/g')" yes | "$sdkmanager" "$pkg" rc=$? (( rc != 0 )) && exit_and_write_code $rc From 49aef7da1372eb461c7b17cccb9e61b7e12bbdf1 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 08:46:18 -0400 Subject: [PATCH 131/256] chore(api): generate types --- api/index.d.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/api/index.d.ts b/api/index.d.ts index cb049a9b51..2a54212076 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -6570,6 +6570,20 @@ declare module "socket:notification" { * @return {Promise} */ export function showNotification(title: string, options?: NotificationOptions | undefined, onclick?: ((arg0: Event) => any) | undefined, onshow?: any): Promise<any>; + /** + * The global event dispatched when a `Notification` is presented to + * the user. + * @ignore + * @type {string} + */ + export const NOTIFICATION_PRESENTED_EVENT: string; + /** + * The global event dispatched when a `Notification` has a response + * from the user. + * @ignore + * @type {string} + */ + export const NOTIFICATION_RESPONSE_EVENT: string; /** * An enumeratino of notification test directions: * - 'auto' Automatically determined by the operating system @@ -6757,8 +6771,9 @@ declare module "socket:notification" { static get maxActions(): number; /** * Requests permission from the user to display notifications. + * @return {Promise<'granted'|'default'|'denied'>} */ - static requestPermission(): Promise<any>; + static requestPermission(): Promise<'granted' | 'default' | 'denied'>; /** * `Notification` class constructor. * @param {string} title @@ -6895,7 +6910,7 @@ declare module "socket:notification" { /** * Closes the notification programmatically. */ - close(): Promise<void>; + close(): Promise<any>; #private; } export default Notification; From c1568414a22eef616b637fabd0dff0122184749c Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 09:24:53 -0400 Subject: [PATCH 132/256] refactor(src/window/linux.cc: use permissions in 'show-notification' --- src/window/linux.cc | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/window/linux.cc b/src/window/linux.cc index ef9634dd26..6c7ef80262 100644 --- a/src/window/linux.cc +++ b/src/window/linux.cc @@ -212,20 +212,8 @@ namespace SSC { WebKitNotification* notification, gpointer userData ) -> bool { - return false; - }), - this - ); - - g_signal_connect( - G_OBJECT(webview), - "close-notification", - G_CALLBACK(+[]( - WebKitWebView* webview, - WebKitNotification* notification, - gpointer userData - ) -> bool { - return false; + static auto userConfig = SSC::getUserConfig(); + return userConfig["permissions_allow_notifications"] == "false"; }), this ); From 9d03c64811952c5281481089cb16ebbe969ec3a0 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 09:47:47 -0400 Subject: [PATCH 133/256] refactor(src/window/linux.cc): handle notification initial permissions from config --- src/window/linux.cc | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/window/linux.cc b/src/window/linux.cc index 6c7ef80262..dee2ae6c57 100644 --- a/src/window/linux.cc +++ b/src/window/linux.cc @@ -204,6 +204,34 @@ namespace SSC { webkit_cookie_manager_set_accept_policy(cookieManager, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS); + g_signal_connect( + G_OBJECT(webContext), + "initialize-notification-permissions", + G_CALLBACK(+[]( + WebKitWebContext* webContext, + gpointer userData + ) { + static const auto userConfig = SSC::getUserConfig(); + static const auto bundleIdentifier = userConfig["meta_bundle_identifier"]; + + GList allowed; + Glist disallowed; + + if (userConfig["permissions_allow_notifications"] == "false") { + g_list_append(&disallowed, bundleIdentifier.c_str()); + } else { + g_list_append(&sallowed, bundleIdentifier.c_str()); + } + + webkit_web_context_initialize_notification_permissions( + webContext, + allowed, + disallowed + ); + }), + this + ); + g_signal_connect( G_OBJECT(webview), "show-notification", From 65d6c4182d335cc646c9323141c95ec03bfa4c7c Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 10:15:38 -0400 Subject: [PATCH 134/256] fix(src/cli/templates.hh): fix typo --- src/cli/templates.hh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/templates.hh b/src/cli/templates.hh index 057223fe5e..8d0e371b1b 100644 --- a/src/cli/templates.hh +++ b/src/cli/templates.hh @@ -870,7 +870,7 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! "DEBUG=1", "SSC_VERSION={{SSC_VERSION}}", "SSC_VERSION_HASH={{SSC_VERSION_HASH}}", - "WAS_CODESIGNED={{WAS_CODESIGNED}}" + "WAS_CODESIGNED={{WAS_CODESIGNED}}", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; From d8d5e5b1bd728be3d5ed8cdbd37623a05112677a Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 10:15:53 -0400 Subject: [PATCH 135/256] fix(src/cli/cli.cc): fix extensions with multiple sources --- src/cli/cli.cc | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 4b9bb4bc77..4088fbc4f5 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -4841,12 +4841,6 @@ int main (const int argc, const char* argv[]) { if (source.size() > 0) { if (fs::is_directory(source)) { settings["build_extensions_" + extension] = ""; - } else { - log( - "\033[33mWARN\033[0m " + key + " is not a directory, ignoring: " + - fs::absolute(source).string() - ); - source = ""; } } } @@ -4868,16 +4862,25 @@ int main (const int argc, const char* argv[]) { } } - if (fs::exists(target)) { + if (fs::exists(target) && fs::is_directory(target)) { target = fs::canonical(target); auto configFile = target / "socket.ini"; auto config = parseINI(fs::exists(configFile) ? readFile(configFile) : ""); settings["build_extensions_" + extension + "_path"] = target.string(); + fs::current_path(target); for (const auto& entry : config) { if (entry.first.starts_with("extension_sources")) { - settings["build_extensions_" + extension] += fs::canonical(target / entry.second); + const auto sources = parseStringList(entry.second, ' '); + Vector<String> canonical; + for (const auto& source : sources) { + log("target = " + target.string()); + log("source = " + source); + canonical.push_back(fs::canonical(target / source)); + } + + settings["build_extensions_" + extension] = join(canonical, " "); } else if (entry.first.starts_with("extension_")) { auto key = replace(entry.first, "extension_", extension + "_"); auto value = entry.second; From c089c3d3e18f1eb5f3aae2dcbb8beecef27711dc Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 11:06:46 -0400 Subject: [PATCH 136/256] refactor(src/window/linux.cc): use dialog when requesting permissions --- src/window/linux.cc | 160 ++++++++++++++++++++++++++++---------------- 1 file changed, 102 insertions(+), 58 deletions(-) diff --git a/src/window/linux.cc b/src/window/linux.cc index dee2ae6c57..0dfc9d7e05 100644 --- a/src/window/linux.cc +++ b/src/window/linux.cc @@ -254,74 +254,75 @@ namespace SSC { WebKitPermissionRequest *request, gpointer userData ) -> bool { + Window* window = reinterpret_cast<Window*>(userData) static auto userConfig = SSC::getUserConfig(); + auto result = false; + String name = ""; + String description = "{{meta_title}} would like permission to use a an unknown feature."; if (WEBKIT_IS_GEOLOCATION_PERMISSION_REQUEST(request)) { - if (userConfig["permissions_allow_geolocation"] != "false") { - webkit_permission_request_allow(request); - return TRUE; - } else { - webkit_permission_request_deny(request); - return FALSE; - } + name = "geolocation"; + result = userConfig["permissions_allow_geolocation"] != "false"; + description = "{{meta_title}} would like access to your location."; } else if (WEBKIT_IS_NOTIFICATION_PERMISSION_REQUEST(request)) { - if (userConfig["permissions_allow_notifications"] != "false") { - webkit_permission_request_allow(request); - return TRUE; - } else { - webkit_permission_request_deny(request); - return FALSE; - } + name = "notifications"; + result = userConfig["permissions_allow_notifications"] != "false"; + description = "{{meta_title}} would like display notifications."; } else if (WEBKIT_IS_USER_MEDIA_PERMISSION_REQUEST(request)) { - if (userConfig["permissions_allow_user_media"] == "false") { - webkit_permission_request_deny(request); - return FALSE; - } else { - if (webkit_user_media_permission_is_for_audio_device(WEBKIT_USER_MEDIA_PERMISSION_REQUEST(request))) { - if (userConfig["permissions_allow_microphone"] == "false") { - webkit_permission_request_deny(request); - return FALSE; - } - } - - if (webkit_user_media_permission_is_for_video_device(WEBKIT_USER_MEDIA_PERMISSION_REQUEST(request))) { - if (userConfig["permissions_allow_camera"] == "false") { - webkit_permission_request_deny(request); - return FALSE; - } - } + if (webkit_user_media_permission_is_for_audio_device(WEBKIT_USER_MEDIA_PERMISSION_REQUEST(request))) { + name = "microphone"; + result = userConfig["permissions_allow_microphone"] == "false"; + description = "{{meta_title}} would like access to your microphone."; + } - webkit_permission_request_allow(request); - return TRUE; + if (webkit_user_media_permission_is_for_video_device(WEBKIT_USER_MEDIA_PERMISSION_REQUEST(request))) { + name = "camera"; + result = userConfig["permissions_allow_camera"] == "false"; + description = "{{meta_title}} would like access to your camera."; } + + result = userConfig["permissions_allow_user_media"] == "false"; } else if (WEBKIT_IS_WEBSITE_DATA_ACCESS_PERMISSION_REQUEST(request)) { - if (userConfig["permissions_allow_data_access"] != "false") { - webkit_permission_request_allow(request); - return TRUE; - } else { - webkit_permission_request_deny(request); - return FALSE; - } + name = "storage-access"; + result = userConfig["permissions_allow_data_access"] != "false"; + description = "{{meta_title}} would like access to local storage."; } else if (WEBKIT_IS_DEVICE_INFO_PERMISSION_REQUEST(request)) { - if (userConfig["permissions_allow_device_info"] != "false") { - webkit_permission_request_allow(request); - return TRUE; - } else { - webkit_permission_request_deny(request); - return FALSE; - } + result = userConfig["permissions_allow_device_info"] != "false"; + description = "{{meta_title}} would like access to your device information."; } else if (WEBKIT_IS_MEDIA_KEY_SYSTEM_PERMISSION_REQUEST(request)) { - if (userConfig["permissions_allow_media_key_system"] != "false") { + result = userConfig["permissions_allow_media_key_system"] != "false"; + description = "{{meta_title}} would like access to your media key system."; + } + + if (result) { + auto title = userConfig["meta_title"]; + GtkWidget *dialog = gtk_message_dialog_new(window->window, + GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_YES_NO, + tmpl(description, userConfig).c_str() + ); + + gtk_widget_show(dialog); + if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_YES) { webkit_permission_request_allow(request); - return TRUE; } else { webkit_permission_request_deny(request); - return FALSE; } + + gtk_widget_destroy(dialog); + } else { + webkit_permission_request_deny(request); } - webkit_permission_request_deny(request); - return FALSE; + if (name.size() > 0) { + JSON::Object json = JSON::Object::Entries { + {"name", name}, + {"state", result ? "granted" : "denied"} + }; + } + + return result; }), this ); @@ -715,15 +716,58 @@ namespace SSC { ) ); + // ALWAYS on or off + webkit_settings_set_enable_webgl(settings, true); webkit_settings_set_zoom_text_only(settings, false); + webkit_settings_set_enable_mediasource(settings, true); + webkit_settings_set_enable_encrypted_media(settings, true); + webkit_settings_set_media_playback_allows_inline(settings, true); + webkit_settings_set_enable_dns_prefetching(settings, true); + + // TODO(@jwerle); make configurable with '[permissions] allow_dialogs' + webkit_settings_set_allow_modal_dialogs( + settings, + true + ); - if (userConfig["permissions_allow_clipboard"] != "false") { - webkit_settings_set_javascript_can_access_clipboard(settings, true); - } + // TODO(@jwerle); make configurable with '[permissions] allow_media' + webkit_settings_set_enable_media(settings, true); + webkit_settings_set_enable_webaudio(settings, true); - if (userConfig["permissions_allow_fullscreen"] != "false") { - webkit_settings_set_enable_fullscreen(settings, true); - } + webkit_settings_set_enable_media_stream( + settings, + userConfig["permissions_allow_user_media"] != "false" + ); + + webkit_settings_set_enable_media_capabilities( + settings, + userConfig["permissions_allow_user_media"] != "false" + ); + + webkit_settings_set_enable_webrtc( + settings, + userConfig["permissions_allow_user_media"] != "false" + ); + + webkit_settings_set_javascript_can_access_clipboard( + settings, + userConfig["permissions_allow_clipboard"] != "false" + ); + + webkit_settings_set_enable_fullscreen( + settings, + userConfig["permissions_allow_fullscreen"] != "false" + ); + + webkit_settings_set_enable_html5_local_storage( + settings, + userConfig["permissions_allow_data_access"] != "false" + ); + + webkit_settings_set_enable_html5_database( + settings, + userConfig["permissions_allow_data_access"] != "false" + ); GdkRGBA rgba = {0}; webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(webview), &rgba); From 0ade075dc0f72649c894de6204f683cc8d7f6d89 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 18:48:54 +0200 Subject: [PATCH 137/256] refactor(src/window/linux.cc): fix deprecations, errors --- src/window/linux.cc | 129 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 100 insertions(+), 29 deletions(-) diff --git a/src/window/linux.cc b/src/window/linux.cc index 0dfc9d7e05..67ddece97c 100644 --- a/src/window/linux.cc +++ b/src/window/linux.cc @@ -48,20 +48,23 @@ namespace SSC { auto value = message.get("value"); auto ctx = new WebViewJavaScriptAsyncContext { reply, message, window }; - webkit_web_view_run_javascript( + webkit_web_view_evaluate_javascript( WEBKIT_WEB_VIEW(window->webview), value.c_str(), + -1, + nullptr, + nullptr, nullptr, [](GObject *object, GAsyncResult *res, gpointer data) { GError *error = nullptr; auto ctx = reinterpret_cast<WebViewJavaScriptAsyncContext*>(data); - auto result = webkit_web_view_run_javascript_finish( + auto value = webkit_web_view_evaluate_javascript_finish( WEBKIT_WEB_VIEW(ctx->window->webview), res, &error ); - if (!result) { + if (!value) { ctx->reply(IPC::Result::Err { ctx->message, JSON::Object::Entries { {"code", error->code}, {"message", String(error->message)} @@ -70,8 +73,6 @@ namespace SSC { g_error_free(error); return; } else { - auto value = webkit_javascript_result_get_js_value(result); - if ( jsc_value_is_null(value) || jsc_value_is_array(value) || @@ -110,8 +111,6 @@ namespace SSC { }}); } } - - webkit_javascript_result_unref(result); }, ctx ); @@ -211,23 +210,43 @@ namespace SSC { WebKitWebContext* webContext, gpointer userData ) { - static const auto userConfig = SSC::getUserConfig(); + static auto userConfig = SSC::getUserConfig(); static const auto bundleIdentifier = userConfig["meta_bundle_identifier"]; - GList allowed; - Glist disallowed; + auto uri = "socket://" + bundleIdentifier; + auto origin = webkit_security_origin_new_for_uri(uri.c_str()); + GList* allowed = nullptr; + GList* disallowed = nullptr; - if (userConfig["permissions_allow_notifications"] == "false") { - g_list_append(&disallowed, bundleIdentifier.c_str()); - } else { - g_list_append(&sallowed, bundleIdentifier.c_str()); + webkit_security_origin_ref(origin); + + if (origin && allowed && disallowed) { + if (userConfig["permissions_allow_notifications"] == "false") { + disallowed = g_list_append(disallowed, (gpointer) origin); + } else { + allowed = g_list_append(allowed, (gpointer) origin); + } + + if (allowed && disallowed) { + webkit_web_context_initialize_notification_permissions( + webContext, + allowed, + disallowed + ); + } } - webkit_web_context_initialize_notification_permissions( - webContext, - allowed, - disallowed - ); + if (allowed) { + g_list_free(allowed); + } + + if (disallowed) { + g_list_free(disallowed); + } + + if (origin) { + webkit_security_origin_unref(origin); + } }), this ); @@ -246,15 +265,54 @@ namespace SSC { this ); + // handle `navigator.permissions.query()` + g_signal_connect( + G_OBJECT(webview), + "query-permission-state", + G_CALLBACK((+[]( + WebKitWebView* webview, + WebKitPermissionStateQuery* query, + gpointer user_data + ) -> bool { + static auto userConfig = SSC::getUserConfig(); + auto name = String(webkit_permission_state_query_get_name(query)); + + if (name == "geolocation") { + webkit_permission_state_query_finish( + query, + userConfig["permissions_allow_geolocation"] == "false" + ? WEBKIT_PERMISSION_STATE_DENIED + : WEBKIT_PERMISSION_STATE_PROMPT + ); + } + + if (name == "notifications") { + webkit_permission_state_query_finish( + query, + userConfig["permissions_allow_notifications"] == "false" + ? WEBKIT_PERMISSION_STATE_DENIED + : WEBKIT_PERMISSION_STATE_PROMPT + ); + } + + webkit_permission_state_query_finish( + query, + WEBKIT_PERMISSION_STATE_PROMPT + ); + return true; + })), + this + ); + g_signal_connect( G_OBJECT(webview), "permission-request", - G_CALLBACK(+[]( + G_CALLBACK((+[]( WebKitWebView* webview, WebKitPermissionRequest *request, gpointer userData ) -> bool { - Window* window = reinterpret_cast<Window*>(userData) + Window* window = reinterpret_cast<Window*>(userData); static auto userConfig = SSC::getUserConfig(); auto result = false; String name = ""; @@ -296,10 +354,12 @@ namespace SSC { if (result) { auto title = userConfig["meta_title"]; - GtkWidget *dialog = gtk_message_dialog_new(window->window, + GtkWidget *dialog = gtk_message_dialog_new( + GTK_WINDOW(window->window), GTK_DIALOG_MODAL, GTK_MESSAGE_QUESTION, GTK_BUTTONS_YES_NO, + "%s", tmpl(description, userConfig).c_str() ); @@ -316,14 +376,14 @@ namespace SSC { } if (name.size() > 0) { - JSON::Object json = JSON::Object::Entries { + JSON::Object::Entries json = JSON::Object::Entries { {"name", name}, {"state", result ? "granted" : "denied"} }; } return result; - }), + })), this ); @@ -417,19 +477,26 @@ namespace SSC { "})()" ); - webkit_web_view_run_javascript( + webkit_web_view_evaluate_javascript( WEBKIT_WEB_VIEW(wv), js.c_str(), + -1, + nullptr, + nullptr, nullptr, [](GObject* src, GAsyncResult* result, gpointer arg) { auto *w = static_cast<Window*>(arg); if (!w) return; GError* error = NULL; - WebKitJavascriptResult* wkr = webkit_web_view_run_javascript_finish(WEBKIT_WEB_VIEW(src), result, &error); - if (!wkr || error) return; + auto value = webkit_web_view_evaluate_javascript_finish( + WEBKIT_WEB_VIEW(src), + result, + &error + ); + + if (!value || error) return; - auto* value = webkit_javascript_result_get_js_value(wkr); if (!jsc_value_is_string(value)) return; JSCException *exception; @@ -858,9 +925,12 @@ namespace SSC { void Window::eval (const String& source) { auto webview = this->webview; this->app.dispatch([=, this] { - webkit_web_view_run_javascript( + webkit_web_view_evaluate_javascript( WEBKIT_WEB_VIEW(this->webview), String(source).c_str(), + -1, + nullptr, + nullptr, nullptr, nullptr, nullptr @@ -891,6 +961,7 @@ namespace SSC { color.alpha = a; gtk_widget_realize(this->window); + // FIXME(@jwerle): this is deprecated gtk_widget_override_background_color( this->window, GTK_STATE_FLAG_NORMAL, &color ); From c868290466ef98479a3a118f077195430c8a6489 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 18:49:03 +0200 Subject: [PATCH 138/256] chore(src/cli/cli.cc): clean up --- src/cli/cli.cc | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 4088fbc4f5..d14e8002f4 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -4875,8 +4875,6 @@ int main (const int argc, const char* argv[]) { const auto sources = parseStringList(entry.second, ' '); Vector<String> canonical; for (const auto& source : sources) { - log("target = " + target.string()); - log("source = " + source); canonical.push_back(fs::canonical(target / source)); } From 61bb32833a9cad28ddbf2ac6d3951e73b24342ea Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 18:49:10 +0200 Subject: [PATCH 139/256] fix(api/url.js): fix exports --- api/url.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/url.js b/api/url.js index 278013c322..b58c9c6df1 100644 --- a/api/url.js +++ b/api/url.js @@ -2,6 +2,6 @@ * @notice This is a rexports of `url/index.js` so consumers will * need to only `import * as url from 'socket:url'` */ -import * as exports from './url/index.js' +import URL from './url/index.js' export * from './url/index.js' -export default exports +export default URL From 560d69ae9aaec177ba6d6feffbe5e3e29ef039ef Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 18:49:24 +0200 Subject: [PATCH 140/256] refactor(api/notification.js): query 'onReady' --- api/notification.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/api/notification.js b/api/notification.js index f66b30a52a..a85c0266b2 100644 --- a/api/notification.js +++ b/api/notification.js @@ -872,11 +872,13 @@ export class Notification extends EventTarget { } } -// listen for 'notification' permission changes where applicable -permissions.query({ name: 'notifications' }).then((result) => { - result.addEventListener('change', () => { - // 'prompt' -> 'default' - state.permission = result.state.replace('prompt', 'default') +hooks.onReady(() => { + // listen for 'notification' permission changes where applicable + permissions.query({ name: 'notifications' }).then((result) => { + result.addEventListener('change', () => { + // 'prompt' -> 'default' + state.permission = result.state.replace('prompt', 'default') + }) }) }) From 63aaf066ef1ca7e904af3bab180ffe59445bc0a7 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 18:50:01 +0200 Subject: [PATCH 141/256] refactor(api/internal/permissions.js): handle 'push' + 'notifications' for linux --- api/internal/permissions.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/api/internal/permissions.js b/api/internal/permissions.js index aa0808f69c..fa361715f7 100644 --- a/api/internal/permissions.js +++ b/api/internal/permissions.js @@ -185,10 +185,6 @@ class PermissionStatus extends EventTarget { * @return {Promise<PermissionStatus>} */ export async function query (descriptor, options) { - if (!isAndroid && !isApple) { - return platform.query(descriptor) - } - if (arguments.length === 0) { throw new TypeError( 'Failed to execute \'query\' on \'Permissions\': ' + @@ -221,6 +217,16 @@ export async function query (descriptor, options) { ) } + if (!isAndroid && !isApple) { + if (isLinux) { + if (name === 'notifications' || name === 'push') { + return new PermissionStatus(name, Notification.permission) + } + } + + return platform.query(descriptor) + } + const result = await ipc.send('permissions.query', { name, signal: options?.signal }) if (result.err) { @@ -271,7 +277,7 @@ export async function request (descriptor, options) { } if (isLinux) { - if (name === 'notifications') { + if (name === 'notifications' || name === 'push') { const currentState = Notification.permission const state = await Notification.requestPermission() From 57e2fe6fca9d43ab9ede2800fbd4a1a580ada20e Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 18:50:16 +0200 Subject: [PATCH 142/256] fix(api/internal/monkeypatch.js): fix patch install --- api/internal/monkeypatch.js | 61 ++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/api/internal/monkeypatch.js b/api/internal/monkeypatch.js index d7a386ba57..b4388ad331 100644 --- a/api/internal/monkeypatch.js +++ b/api/internal/monkeypatch.js @@ -12,30 +12,42 @@ let applied = false export function init () { if (applied || !globalThis.window) return - function install (name, implementation, target = globalThis, prefix) { - if (typeof name === 'object') { - for (const key in name) { - install(key, name[key], target || implementation, prefix || target) - } - return - } + function install (implementations, target = globalThis, prefix) { + for (let name in implementations) { + const implementation = implementations[name] - const actualName = name.split('.').slice(-1)[0] - - if (typeof prefix === 'string') { - name = `${prefix}.${name}` - } + if (typeof prefix === 'string') { + name = `${prefix}.${name}` + } - if (typeof target[actualName] === 'object' && target[actualName] !== null) { - for (const key in implementation) { - const nativeImplementation = target[actualName][key] || null + const actualName = name.split('.').slice(-1)[0] + + if (typeof target[actualName] === 'object' && target[actualName] !== null) { + for (const key in implementation) { + const nativeImplementation = target[actualName][key] || null + // let this fail, the environment implementation may not be writable + try { + target[actualName][key] = implementation[key] + } catch {} + + if (nativeImplementation !== null) { + const nativeName = ['_', 'native', ...name.split('.'), key].join('_') + Object.defineProperty(globalThis, nativeName, { + enumerable: false, + configurable: false, + value: nativeImplementation + }) + } + } + } else { + const nativeImplementation = target[actualName] || null // let this fail, the environment implementation may not be writable try { - target[actualName][key] = implementation[key] + target[actualName] = implementation } catch {} if (nativeImplementation !== null) { - const nativeName = ['_', 'native', ...name.split('.'), key].join('_') + const nativeName = ['_', 'native', ...name.split('.')].join('_') Object.defineProperty(globalThis, nativeName, { enumerable: false, configurable: false, @@ -43,21 +55,6 @@ export function init () { }) } } - } else { - const nativeImplementation = target[actualName] || null - // let this fail, the environment implementation may not be writable - try { - target[actualName] = implementation - } catch {} - - if (nativeImplementation !== null) { - const nativeName = ['_', 'native', ...name.split('.')].join('_') - Object.defineProperty(globalThis, nativeName, { - enumerable: false, - configurable: false, - value: nativeImplementation - }) - } } } From 02a786f0bcd46b64f242f92d1ddb5b3f222c14d3 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 18:51:37 +0200 Subject: [PATCH 143/256] chore(api): generate types --- api/index.d.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/api/index.d.ts b/api/index.d.ts index 2a54212076..9ebdaa0f84 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -481,8 +481,8 @@ declare module "socket:url/index" { } declare module "socket:url" { export * from "socket:url/index"; - export default exports; - import * as exports from "socket:url/index"; + export default URL; + import URL from "socket:url/index"; } declare module "socket:util" { export function hasOwnProperty(object: any, property: any): any; @@ -5359,7 +5359,7 @@ declare module "socket:module" { stream: typeof stream; test: typeof test; util: typeof util; - url: typeof url; + url: any; }; export const builtinModules: { buffer: typeof buffer; @@ -5383,7 +5383,7 @@ declare module "socket:module" { stream: typeof stream; test: typeof test; util: typeof util; - url: typeof url; + url: any; }; /** * CommonJS module scope source wrapper. @@ -5553,7 +5553,6 @@ declare module "socket:module" { import stream from "socket:stream"; import test from "socket:test"; import util from "socket:util"; - import url from "socket:url"; } declare module "socket:stream-relay/packets" { From ca014922f717846a19c8a303cfb313beacd7bb7d Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 14:15:46 -0400 Subject: [PATCH 144/256] fix(bin/install.sh): fix webview version typo --- bin/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/install.sh b/bin/install.sh index 874868cea9..d73cd59873 100755 --- a/bin/install.sh +++ b/bin/install.sh @@ -354,7 +354,7 @@ function _get_web_view2() { echo "# Downloading Webview2" - curl -L https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/1.0.2045.177 --output "$tmp/webview2.zip" + curl -L https://www.nuget.org/api/v2/package/Microsoft.Web.WebView2/1.0.2045.28 --output "$tmp/webview2.zip" cd "$tmp" || exit 1 unzip -q "$tmp/webview2.zip" mkdir -p "$BUILD_DIR/include" From 65fccb2770471852dfb55dca80e41b37941ed491 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 14:16:09 -0400 Subject: [PATCH 145/256] fix(src/cli/cli.cc): convert path to string --- src/cli/cli.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index d14e8002f4..3223434a0e 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -4875,7 +4875,7 @@ int main (const int argc, const char* argv[]) { const auto sources = parseStringList(entry.second, ' '); Vector<String> canonical; for (const auto& source : sources) { - canonical.push_back(fs::canonical(target / source)); + canonical.push_back(fs::canonical(target / source).string()); } settings["build_extensions_" + extension] = join(canonical, " "); From 021dbfc33b26b9e113c5a8280e2053e1af60fe9a Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 14:16:44 -0400 Subject: [PATCH 146/256] test(extension/sqlite3): simplify set/get --- include/socket/extension.h | 6 +++--- test/src/extensions/sqlite3/extension.cc | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/include/socket/extension.h b/include/socket/extension.h index 8720f8fb35..084ce377c1 100644 --- a/include/socket/extension.h +++ b/include/socket/extension.h @@ -462,9 +462,9 @@ extern "C" { sapi_json_object_set_value( \ (sapi_json_object_t*) (object), \ (const char*)(key), \ - sapi_json_any((value)) \ - ) + (sapi_json_any((value))) \ + ) /** * Set JSON `value` for JSON `array` at `index`. Generally, an alias to the * `sapi_json_array_set` function. @@ -476,7 +476,7 @@ extern "C" { sapi_json_array_set_value( \ (sapi_json_array_t*) (array), \ (unsigned int) (index), \ - sapi_json_any((value)) \ + (sapi_json_any((value))) \ ) /** diff --git a/test/src/extensions/sqlite3/extension.cc b/test/src/extensions/sqlite3/extension.cc index c96f53ab99..dea474b6ac 100644 --- a/test/src/extensions/sqlite3/extension.cc +++ b/test/src/extensions/sqlite3/extension.cc @@ -26,7 +26,7 @@ void onexec ( sapi_json_object_set( err, "message", - sapi_json_any(sapi_json_string_create(context, "Missing 'query' in parameters")) + sapi_json_string_create(context, "Missing 'query' in parameters") ); sapi_ipc_result_set_json_error(result, sapi_json_any(err)); sapi_ipc_reply(result); @@ -38,7 +38,7 @@ void onexec ( sapi_json_object_set( err, "message", - sapi_json_any(sapi_json_string_create(context, "Missing 'id' in parameters")) + sapi_json_string_create(context, "Missing 'id' in parameters") ); sapi_ipc_result_set_json_error(result, sapi_json_any(err)); sapi_ipc_reply(result); @@ -50,12 +50,12 @@ void onexec ( sapi_json_object_set( err, "type", - sapi_json_any(sapi_json_string_create(context, "NotFoundError")) + sapi_json_string_create(context, "NotFoundError") ); sapi_json_object_set( err, "message", - sapi_json_any(sapi_json_string_create(context, "Database not found")) + sapi_json_string_create(context, "Database not found") ); sapi_ipc_result_set_json_error(result, sapi_json_any(err)); sapi_ipc_reply(result); @@ -71,7 +71,7 @@ void onexec ( sapi_json_object_set( err, "message", - sapi_json_any(sapi_json_string_create(context, sqlite3_errmsg(db))) + sapi_json_string_create(context, sqlite3_errmsg(db)) ); sapi_ipc_result_set_json_error(result, sapi_json_any(err)); sapi_ipc_reply(result); @@ -96,7 +96,7 @@ void onexec ( sapi_json_object_set( row, name, - sapi_json_any(sapi_json_number_create(context, value)) + sapi_json_number_create(context, value) ); break; } From 1ab98d263ab90cda1265517a3d0e699f9e36aa3f Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 14:18:11 -0400 Subject: [PATCH 147/256] fix(api/url/index.js): include 'resolve' in 'URL' --- api/url/index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/api/url/index.js b/api/url/index.js index 03fba252ec..8a8851bac7 100644 --- a/api/url/index.js +++ b/api/url/index.js @@ -9,15 +9,14 @@ const { serializeHost } = url -export default URL -export { URLPattern, URL, URLSearchParams, parseURL } - for (const key in globalThis.URL) { if (!URL[key]) { URL[key] = globalThis.URL[key].bind(globalThis.URL) } } +URL.resolve = resolve + export const parse = parseURL // lifted from node @@ -41,3 +40,6 @@ url.serializeURLOrigin = function (input) { return serializeURLOrigin(input) } + +export default URL +export { URLPattern, URL, URLSearchParams, parseURL } From 83d886a25034d88e0b56f7281f1a957e7bd45c87 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 14:18:28 -0400 Subject: [PATCH 148/256] fix(api/notification.js): add 'NativeNotification' fallback --- api/notification.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/notification.js b/api/notification.js index a85c0266b2..2f42052464 100644 --- a/api/notification.js +++ b/api/notification.js @@ -16,7 +16,10 @@ import ipc from './ipc.js' import os from './os.js' const isLinux = os.platform() === 'linux' -const NativeNotification = globalThis.Notification +const NativeNotification = ( + globalThis.Notification || + class NativeNotification extends EventTarget {} +) /** * Used to determine if notification beign created in a `ServiceWorker`. From 35044e5834eef13129d8701debc668b80f78fbd6 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 20:26:07 +0200 Subject: [PATCH 149/256] fix(include/socket/extension.h): fix typo --- include/socket/extension.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/socket/extension.h b/include/socket/extension.h index 084ce377c1..dffdecddd3 100644 --- a/include/socket/extension.h +++ b/include/socket/extension.h @@ -463,8 +463,8 @@ extern "C" { (sapi_json_object_t*) (object), \ (const char*)(key), \ (sapi_json_any((value))) \ - ) + /** * Set JSON `value` for JSON `array` at `index`. Generally, an alias to the * `sapi_json_array_set` function. @@ -476,7 +476,7 @@ extern "C" { sapi_json_array_set_value( \ (sapi_json_array_t*) (array), \ (unsigned int) (index), \ - (sapi_json_any((value))) \ + (sapi_json_any((value))) \ ) /** From d19f88d387bfcc9e713099eb8be2000de67dfb3f Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 14:46:21 -0400 Subject: [PATCH 150/256] fix(src/common.hh): fix size check --- src/common.hh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common.hh b/src/common.hh index ec249190f3..13351be652 100644 --- a/src/common.hh +++ b/src/common.hh @@ -641,7 +641,7 @@ namespace SSC { } inline void stdWrite (const String &str, bool isError) { - static const auto IN_GITHUB_ACTIONS_CI = getEnv("GITHUB_ACTIONS_CI").size() == 0; + static const auto IN_GITHUB_ACTIONS_CI = getEnv("GITHUB_ACTIONS_CI").size() > 0; auto& stream = isError ? std::cerr : std::cout; stream << str; From 8f4b567ea914c0992c407fcdefd08116f5bcb2b1 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 15:44:38 -0400 Subject: [PATCH 151/256] refactor(src/ipc/ipc.hh): track emulator state in bridge --- src/ipc/ipc.hh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ipc/ipc.hh b/src/ipc/ipc.hh index d5f986e92d..f36be6bb7e 100644 --- a/src/ipc/ipc.hh +++ b/src/ipc/ipc.hh @@ -287,6 +287,10 @@ namespace SSC::IPC { FileSystemWatcher* fileSystemWatcher = nullptr; #endif + #if defined(__ANDROID__) + std::atomic<bool> isAndroidEmulator = false; + #endif + Bridge (Core *core); ~Bridge (); bool route (const String& msg, const char *bytes, size_t size); From 59f120e1d4de1c99383892f05fb76c1be09eeb3f Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 15:45:53 -0400 Subject: [PATCH 152/256] refactor(src/ipc/bridge.cc): introduce 'host-operating-system' primordial property --- src/ipc/bridge.cc | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/ipc/bridge.cc b/src/ipc/bridge.cc index baff8722c3..2ace1e32d0 100644 --- a/src/ipc/bridge.cc +++ b/src/ipc/bridge.cc @@ -1782,6 +1782,27 @@ static void initRouterTable (Router *router) { {"full", SSC::VERSION_FULL_STRING}, {"short", SSC::VERSION_STRING}, {"hash", SSC::VERSION_HASH_STRING}} + }, + {"host-operating-system", + #if defined(__APPLE__) + #if TARGET_OS_IPHONE + "iphoneos" + #elif TARGET_IPHONE_SIMULATOR + "iphonesimulator" + #else + "macosx" + #endif + #elif defined(__ANDROID__) + (router->bridge->isAndroidEmulator ? "android-emulator" : "android") + #elif defined(__WIN32) + "win32" + #elif defined(__linux__) + "linux" + #elif defined(__unix__) || defined(__unix) + "unix" + #else + "unknown" + #endif } }} }; From 0213817191c68504cd99f201fde9002693720979 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 15:46:43 -0400 Subject: [PATCH 153/256] refactor(android): detect emulator and report to runtime --- src/android/bridge.cc | 2 ++ src/android/internal.hh | 1 + src/android/main.kt | 23 +++++++++++++++++++++++ src/android/runtime.cc | 17 +++++++++++++++++ 4 files changed, 43 insertions(+) diff --git a/src/android/bridge.cc b/src/android/bridge.cc index 359f783bfa..62eccca7b6 100644 --- a/src/android/bridge.cc +++ b/src/android/bridge.cc @@ -20,6 +20,8 @@ namespace SSC::android { callback(); } }; + + this->isAndroidEmulator = this->runtime->isEmulator; } Bridge::~Bridge () { diff --git a/src/android/internal.hh b/src/android/internal.hh index 4986cea50b..783c28ff1a 100644 --- a/src/android/internal.hh +++ b/src/android/internal.hh @@ -215,6 +215,7 @@ namespace SSC::android { jobject self = nullptr; jlong pointer = 0; String rootDirectory = ""; + std::atomic<bool> isEmulator = false; Runtime (JNIEnv* env, jobject self, String rootDirectory); ~Runtime (); diff --git a/src/android/main.kt b/src/android/main.kt index 2889d309d7..71bee625c3 100644 --- a/src/android/main.kt +++ b/src/android/main.kt @@ -104,6 +104,29 @@ open class MainActivity : WebViewActivity() { } )) + this.runtime.setIsEmulator( + ( + android.os.Build.BRAND.startsWith("generic") && + android.ios.Build.DEVICE.startsWith("generic") + ) || + android.os.Build.FINGERPRINT.startsWith("generic") || + android.os.Build.FINGERPRINT.startsWith("unknown") || + android.os.Build.HARDWARE.contains("goldfish") || + android.os.Build.HARDWARE.contains("ranchu") || + android.os.Build.MODEL.contains("google_sdk") || + android.os.Build.MODEL.contains("Emulator") || + android.os.Build.MODEL.contains("Android SDK built for x86") || + android.os.Build.MANUFACTURER.contains("Genymotion") || + android.os.Build.PRODUCT.contains("sdk_google") || + android.os.Build.PRODUCT.contains("google_sdk") || + android.os.Build.PRODUCT.contains("sdk") || + android.os.Build.PRODUCT.contains("sdk_x86") || + android.os.Build.PRODUCT.contains("sdk_gphone64_arm64") || + android.os.Build.PRODUCT.contains("vbox86p") || + android.os.Build.PRODUCT.contains("emulator") || + android.os.Build.PRODUCT.contains("simulator") || + ) + this.window = Window(this.runtime, this) this.window.load() diff --git a/src/android/runtime.cc b/src/android/runtime.cc index a278869a7a..ac26938096 100644 --- a/src/android/runtime.cc +++ b/src/android/runtime.cc @@ -177,4 +177,21 @@ extern "C" { return runtime->isPermissionAllowed(name); } + + jboolean external(Runtime, setIsEmulator)( + JNIEnv *env, + jobject self, + jboolean value + ) { + auto runtime = Runtime::from(env, self); + auto name = StringWrap(env, permission).str(); + + if (runtime == nullptr) { + Throw(env, RuntimeNotInitializedException); + return false; + } + + runtime->isEmulator = value; + return true; + } } From 712bd5e339d40972916bc29ac0400e21169830a1 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 15:47:06 -0400 Subject: [PATCH 154/256] feat(api/os): introduce 'os.host()' for actual host OS detection --- api/os.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/api/os.js b/api/os.js index f8bdd11f22..cff6db835c 100644 --- a/api/os.js +++ b/api/os.js @@ -360,6 +360,24 @@ export function availableMemory () { return result.data.readBigUInt64BE(0) } +/** + * The host operating system. This value can be one of: + * - android + * - android-emulator + * - iphoneos + * - iphone-simulator + * - linux + * - macosx + * - unix + * - unknown + * - win32 + * @ignore + * @return {'android'|'android-emulator'|'iphoneos'|iphone-simulator'|'linux'|'macosx'|unix'|unknown'|win32'} + */ +export function host () { + return primordials['host-operating-system'] || 'unknown' +} + // eslint-disable-next-line import * as exports from './os.js' export default exports From 9cad6559c544d8884ccedbe3fbac5a2ea2c8633e Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 15:47:19 -0400 Subject: [PATCH 155/256] refactor(api/notification.js): do not query on emulator/simulator --- api/notification.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/notification.js b/api/notification.js index 2f42052464..e2765f1434 100644 --- a/api/notification.js +++ b/api/notification.js @@ -876,6 +876,10 @@ export class Notification extends EventTarget { } hooks.onReady(() => { + if (os.host() === 'iphone-simulator' || os.host() === 'android-emulator') { + return + } + // listen for 'notification' permission changes where applicable permissions.query({ name: 'notifications' }).then((result) => { result.addEventListener('change', () => { From 5eca741d507408fa54ac25893768bd108e829dcf Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 15:47:34 -0400 Subject: [PATCH 156/256] test(ipc): check for 'host-operating-system' primodial property --- test/src/ipc.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/src/ipc.js b/test/src/ipc.js index 38650ad698..a32138b0e5 100644 --- a/test/src/ipc.js +++ b/test/src/ipc.js @@ -43,7 +43,8 @@ test('primordials', (t) => { 'arch', 'cwd', 'platform', - 'version' + 'version', + 'host-operating-system' ].sort(), 'primordials keys match') t.equal(typeof primordials.arch, 'string', 'primordials.arch is a string') t.equal(typeof primordials.cwd, 'string', 'primordials.cwd is a string') From 748de4195003d26d48dd574d8c3dd6a76b646c4b Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 15:52:53 -0400 Subject: [PATCH 157/256] chore(api): generate types --- api/index.d.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/api/index.d.ts b/api/index.d.ts index 9ebdaa0f84..7578613913 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -471,8 +471,8 @@ declare module "socket:url/url/url" { } declare module "socket:url/index" { export function resolve(from: any, to: any): any; - export default URL; export const parse: any; + export default URL; export const URL: any; import { URLPattern } from "socket:url/urlpattern/urlpattern"; export const URLSearchParams: any; @@ -728,6 +728,21 @@ declare module "socket:os" { * @ignore */ export function availableMemory(): any; + /** + * The host operating system. This value can be one of: + * - android + * - android-emulator + * - iphoneos + * - iphone-simulator + * - linux + * - macosx + * - unix + * - unknown + * - win32 + * @ignore + * @return {'android'|'android-emulator'|'iphoneos'|iphone-simulator'|'linux'|'macosx'|unix'|unknown'|win32'} + */ + export function host(): 'android' | 'android-emulator' | 'iphoneos' | iphone; /** * @type {string} * The operating system's end-of-line marker. `'\r\n'` on Windows and `'\n'` on POSIX. From 9f6ae8b04828ba6d2483d2fe0645fd4e3bc5c890 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 16:17:53 -0400 Subject: [PATCH 158/256] refactor(android,ipc): remove 'std::atomic' --- src/android/internal.hh | 2 +- src/ipc/ipc.hh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/android/internal.hh b/src/android/internal.hh index 783c28ff1a..6c20abfb47 100644 --- a/src/android/internal.hh +++ b/src/android/internal.hh @@ -215,7 +215,7 @@ namespace SSC::android { jobject self = nullptr; jlong pointer = 0; String rootDirectory = ""; - std::atomic<bool> isEmulator = false; + bool isEmulator = false; Runtime (JNIEnv* env, jobject self, String rootDirectory); ~Runtime (); diff --git a/src/ipc/ipc.hh b/src/ipc/ipc.hh index f36be6bb7e..a62616acdc 100644 --- a/src/ipc/ipc.hh +++ b/src/ipc/ipc.hh @@ -288,7 +288,7 @@ namespace SSC::IPC { #endif #if defined(__ANDROID__) - std::atomic<bool> isAndroidEmulator = false; + bool isAndroidEmulator = false; #endif Bridge (Core *core); From 7c8a7b9479b8206009bcabc6c549f2f86f2810d2 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 16:23:08 -0400 Subject: [PATCH 159/256] fix(src/config.hh): include 'string' header --- src/config.hh | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/config.hh b/src/config.hh index 1c7f829fa4..e86988d708 100644 --- a/src/config.hh +++ b/src/config.hh @@ -1,10 +1,7 @@ #ifndef SSC_CONFIG_H #define SSC_CONFIG_H -#ifndef DEBUG -#define DEBUG 0 -#endif - +// TODO(@jwerle): remove this and any need for it #ifndef SSC_SETTINGS #define SSC_SETTINGS "" #endif @@ -17,16 +14,24 @@ #define SSC_VERSION_HASH "" #endif +// TODO(@jwerle): stop using this and prefer a namespaced macro +#ifndef DEBUG +#define DEBUG 0 +#endif + +// TODO(@jwerle): stop using this and prefer a namespaced macro #ifndef HOST #define HOST "localhost" #endif +// TODO(@jwerle): stop using this and prefer a namespaced macro #ifndef PORT #define PORT 0 #endif #if defined(__cplusplus) #include <map> +#include <string> namespace SSC { // from init.cc From ecbb973d871f224ac7205aa9b763d7fe5da00aeb Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Wed, 11 Oct 2023 23:07:50 +0200 Subject: [PATCH 160/256] socket-node 0.4.4 --- npm/packages/@socketsupply/socket-node/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm/packages/@socketsupply/socket-node/package.json b/npm/packages/@socketsupply/socket-node/package.json index 259e1d3878..1036ecf030 100644 --- a/npm/packages/@socketsupply/socket-node/package.json +++ b/npm/packages/@socketsupply/socket-node/package.json @@ -1,6 +1,6 @@ { "name": "@socketsupply/socket-node", - "version": "0.1.2", + "version": "0.4.0", "description": "A Node.js adapter for the Socket SDK", "main": "index.js", "exports": { From fe28d8fc5fbef5a8614797156134e88174dd5d0d Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Wed, 11 Oct 2023 21:20:38 +0200 Subject: [PATCH 161/256] chore(bin/version.sh): sync socket-node with main package --- bin/version-npm-modules.sh | 29 ----------------------------- bin/version.sh | 10 ++++++++++ 2 files changed, 10 insertions(+), 29 deletions(-) delete mode 100755 bin/version-npm-modules.sh diff --git a/bin/version-npm-modules.sh b/bin/version-npm-modules.sh deleted file mode 100755 index 003bbf851b..0000000000 --- a/bin/version-npm-modules.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -declare root="$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd)" -declare archs=() -source "$root/bin/functions.sh" - - -declare platform="$(lower "$(host_os)")" - -if [ "$(uname -m)" = "x86_64" ]; then - archs+=("x64") -elif [ "$(uname -m)" = "aarch64" ]; then - archs+=("arm64") -else - archs+=("$(uname -m)") -fi - -if [[ "$platform" = "linux" ]]; then - if [ -n "$WSL_DISTRO_NAME" ] || uname -r | grep 'Microsoft'; then - platform="win32" - fi -fi - -for arch in "${archs[@]}"; do - declare package="@socketsupply/socket-$platform-$arch" - cd "$root/npm/packages/$package" || exit $? - echo "# $package" - npm version "$@" || exit $? -done diff --git a/bin/version.sh b/bin/version.sh index 43907e8623..f621b4334e 100755 --- a/bin/version.sh +++ b/bin/version.sh @@ -19,6 +19,16 @@ fi echo -e "Will set new version to be $INPUT_STRING" echo $INPUT_STRING > VERSION.txt jq ".version = \"$INPUT_STRING\"" clib.json > tmp.$$.json && mv tmp.$$.json clib.json + +BASE_LIST=($(echo $INPUT_STRING | tr '.' ' ')) +V_MAJOR_NEW=${BASE_LIST[0]} +V_MINOR_NEW=${BASE_LIST[1]} + +if [ "$V_MAJOR" -ne "$V_MAJOR_NEW" ] || [ "$V_MINOR" -ne "$V_MINOR_NEW" ]; then + cd npm/packages/@socketsupply/socket-node + npm version $V_MAJOR_NEW.$V_MINOR_NEW.0 +fi + # git add VERSION.txt clib.json # git commit -m "Bump version to ${INPUT_STRING}." # git tag -a -m "Tag version ${INPUT_STRING}." "v$INPUT_STRING" From 350f9cf789d5438ee575a1c3752fc4c3e3271a82 Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Wed, 11 Oct 2023 22:13:04 +0200 Subject: [PATCH 162/256] feat(ci): add socket-node version check --- bin/ci_version_check.ps1 | 20 +++++++++++++++++++- bin/ci_version_check.sh | 20 +++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/bin/ci_version_check.ps1 b/bin/ci_version_check.ps1 index 465a50f020..d9690f57d1 100644 --- a/bin/ci_version_check.ps1 +++ b/bin/ci_version_check.ps1 @@ -12,10 +12,28 @@ $VERSION_EXPECTED = "$VERSION_TXT ($VERSION_GIT)" if ($VERSION_SSC -eq $VERSION_EXPECTED) { Write-Output "Version check has passed" - Exit 0 } else { Write-Output "Version check has failed" Write-Output "Expected: $VERSION_EXPECTED" Write-Output "Got: $VERSION_SSC" Exit 1 } + +$BASE_LIST = $VERSION_TXT -split '\.' + +$V_MAJOR = $BASE_LIST[0] +$V_MINOR = $BASE_LIST[1] + +$VERSION_NODE = npm show ./npm/packages/@socketsupply/socket-node version + +$BASE_LIST = $VERSION_NODE -split '\.' + +$V_MAJOR_NODE = $BASE_LIST_NODE[0] +$V_MINOR_NODE = $BASE_LIST_NODE[1] + +if (($V_MAJOR -ne $V_MAJOR_NODE) -or ($V_MINOR -ne $V_MINOR_NODE)) { + Write-Output "Version of @socketsupply/socket-node is not in sync with @socketsupply/socket" + Write-Output "@socketsupply/socket version is $VERSION_TXT" + Write-Output "@socketsupply/socket-node version is $VERSION_NODE" + exit 1 +} diff --git a/bin/ci_version_check.sh b/bin/ci_version_check.sh index 4ddcb51873..d85a4801f3 100755 --- a/bin/ci_version_check.sh +++ b/bin/ci_version_check.sh @@ -8,10 +8,28 @@ VERSION_EXPECTED="$VERSION_TXT ($VERSION_GIT)" if [ "$VERSION_SSC" = "$VERSION_EXPECTED" ]; then echo "Version check has passed"; - exit 0; else echo "Version check has failed"; echo "Expected: $VERSION_EXPECTED"; echo "Got: $VERSION_SSC"; exit 1; fi + +BASE_LIST=($(echo $VERSION_TXT | tr '.' ' ')) + +V_MAJOR=${BASE_LIST[0]} +V_MINOR=${BASE_LIST[1]} + +VERSION_NODE=$(npm show ./npm/packages/@socketsupply/socket-node version) + +BASE_LIST=($(echo $VERSION_NODE | tr '.' ' ')) + +V_MAJOR_NODE=${BASE_LIST[0]} +V_MINOR_NODE=${BASE_LIST[1]} + +if [ "$V_MAJOR" -ne "$V_MAJOR_NODE" ] || [ "$V_MINOR" -ne "$V_MINOR_NODE" ]; then + echo "Version of @socketsupply/socket-node is not in sync with @socketsupply/socket"; + echo "@socketsupply/socket version is $VERSION_TXT"; + echo "@socketsupply/socket-node version is $VERSION_NODE"; + exit 1; +fi From bb79705c51eaf610183d5b8442bd335522b2af39 Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Wed, 11 Oct 2023 22:32:45 +0200 Subject: [PATCH 163/256] fix(ci): fix versions check on windows --- bin/ci_version_check.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/ci_version_check.ps1 b/bin/ci_version_check.ps1 index d9690f57d1..d46a67ae47 100644 --- a/bin/ci_version_check.ps1 +++ b/bin/ci_version_check.ps1 @@ -28,8 +28,8 @@ $VERSION_NODE = npm show ./npm/packages/@socketsupply/socket-node version $BASE_LIST = $VERSION_NODE -split '\.' -$V_MAJOR_NODE = $BASE_LIST_NODE[0] -$V_MINOR_NODE = $BASE_LIST_NODE[1] +$V_MAJOR_NODE = $BASE_LIST[0] +$V_MINOR_NODE = $BASE_LIST[1] if (($V_MAJOR -ne $V_MAJOR_NODE) -or ($V_MINOR -ne $V_MINOR_NODE)) { Write-Output "Version of @socketsupply/socket-node is not in sync with @socketsupply/socket" From 4f29e4cdc747e3ff2a2e3beed5247e6a5110a857 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:22:07 -0400 Subject: [PATCH 164/256] feat(extension): add `sapi_ipc_send_chunk` and `sapi_ipc_send_event` - Before calling sapi_ipc_send_chunk, you must set the Transfer-Encoding header to `chunked` and then call sapi_ipc_reply. - When using sapi_ipc_send_chunk, the frontend should use XMLHttpRequest. Currently, the Fetch API does not support response streaming in WebKit. - Before calling sapi_ipc_send_event, you must set the Content-Type header to `text/event-stream` and then call sapi_ipc_reply. - When using sapi_ipc_send_event, the frontend should use EventSource. --- include/socket/extension.h | 45 ++++++++++++++++++++++ src/core/core.hh | 2 + src/extension/ipc.cc | 75 ++++++++++++++++++++++++++++++++++--- src/ipc/bridge.cc | 77 ++++++++++++++++++++++++++++++-------- src/ipc/ipc.cc | 1 + src/ipc/ipc.hh | 7 ++++ 6 files changed, 185 insertions(+), 22 deletions(-) diff --git a/include/socket/extension.h b/include/socket/extension.h index dffdecddd3..31d569a066 100644 --- a/include/socket/extension.h +++ b/include/socket/extension.h @@ -1140,6 +1140,51 @@ extern "C" { const sapi_json_any_t* json ); + /** + * Write a chunk to the HTTP response associated with the IPC result. + * + * โš ๏ธ You must have called `sapi_ipc_result_set_header` with a `Transfer-Encoding` + * header of `chunked` to use this function. + * + * โš ๏ธ You must call `sapi_ipc_reply` before calling this function. + * + * @param result - An IPC request result + * @param chunk - The chunk to write + * @param chunk_size - The size of the chunk + * @param finished - `true` if this is the last chunk to write + */ + SOCKET_RUNTIME_EXTENSION_EXPORT + bool sapi_ipc_send_chunk ( + sapi_ipc_result_t* result, + const char* chunk, + size_t chunk_size, + bool finished + ); + + /** + * Write an event to the HTTP response associated with the IPC result. + * + * โš ๏ธ You must have called `sapi_ipc_result_set_header` with a `Content-Type` + * header of `text/event-stream` to use this function. + * + * โš ๏ธ You must call `sapi_ipc_reply` before calling this function. + * + * โš ๏ธ The `name` and `data` arguments must be null-terminated strings. Either + * can be empty as long as it's null-terminated and the other is not empty. + * + * @param result - An IPC request result + * @param name - The event name + * @param data - The event data + * @param finished - `true` if this is the last event to write + */ + SOCKET_RUNTIME_EXTENSION_EXPORT + bool sapi_ipc_send_event ( + sapi_ipc_result_t* result, + const char* name, + const char* data, + bool finished + ); + /** * Creates a "reply" for an IPC route request. * @param result - An IPC request result diff --git a/src/core/core.hh b/src/core/core.hh index 85492743aa..f8f5659cc9 100644 --- a/src/core/core.hh +++ b/src/core/core.hh @@ -126,6 +126,8 @@ namespace SSC { char* body = nullptr; size_t length = 0; String headers = ""; + std::shared_ptr<std::function<bool(const char*, const char*, bool)>> event_stream; + std::shared_ptr<std::function<bool(const char*, size_t, bool)>> chunk_stream; }; using Posts = std::map<uint64_t, Post>; diff --git a/src/extension/ipc.cc b/src/extension/ipc.cc index 6373648ee7..042173baa3 100644 --- a/src/extension/ipc.cc +++ b/src/extension/ipc.cc @@ -32,12 +32,7 @@ bool sapi_ipc_router_map ( auto router, auto reply ) mutable { - auto msg = SSC::IPC::Message( - message.uri, - true, // decode parameter values AOT in `message.uri` - message.buffer.bytes, - message.buffer.size - ); + auto msg = SSC::IPC::Message(message); context->internal = new SSC::IPC::Router::ReplyCallback(reply); callback( context, @@ -150,6 +145,56 @@ bool sapi_ipc_reply (const sapi_ipc_result_t* result) { return success; } +bool sapi_ipc_send_chunk ( + sapi_ipc_result_t* result, + const char* chunk, + size_t chunk_size, + bool finished +) { + if (result == nullptr) { + return false; + } + if (!result->message.isHTTP) { + std::string error = + "IPC method '" + result->message.name + "' must be invoked with HTTP"; + return sapi_ipc_reply_with_error(result, error.c_str()); + } + auto send_chunk_ptr = result->post.chunk_stream; + if (send_chunk_ptr == nullptr) { + return false; + } + bool success = (*send_chunk_ptr)(chunk, chunk_size, finished); + if (finished) { + sapi_context_release(result->context); + } + return success; +} + +bool sapi_ipc_send_event ( + sapi_ipc_result_t* result, + const char* name, + const char* data, + bool finished +) { + if (result == nullptr) { + return false; + } + if (!result->message.isHTTP) { + std::string error = + "IPC method '" + result->message.name + "' must be invoked with HTTP"; + return sapi_ipc_reply_with_error(result, error.c_str()); + } + auto send_event_ptr = result->post.event_stream; + if (send_event_ptr == nullptr) { + return false; + } + bool success = (*send_event_ptr)(name, data, finished); + if (finished) { + sapi_context_release(result->context); + } + return success; +} + bool sapi_ipc_send_bytes ( sapi_context_t* ctx, sapi_ipc_message_t* message, @@ -609,6 +654,24 @@ void sapi_ipc_result_set_header ( ) { if (result && name && value) { result->headers.set(name, value); + + if (strcasecmp(name, "content-type") == 0 && + strcasecmp(value, "text/event-stream") == 0) { + result->post = SSC::Post(); + result->post.event_stream = + std::make_shared<std::function<bool(const char*, const char*, bool)>>( + [result](const char* name, const char* data, bool finished) { + return false; + }); + } else if (strcasecmp(name, "transfer-encoding") == 0 && + strcasecmp(value, "chunked") == 0) { + result->post = SSC::Post(); + result->post.chunk_stream = + std::make_shared<std::function<bool(const char*, size_t, bool)>>( + [result](const char* chunk, size_t chunk_size, bool finished) { + return false; + }); + } } } diff --git a/src/ipc/bridge.cc b/src/ipc/bridge.cc index 2ace1e32d0..ac286c4f3d 100644 --- a/src/ipc/bridge.cc +++ b/src/ipc/bridge.cc @@ -2340,8 +2340,8 @@ static void registerSchemeHandler (Router *router) { auto request = task.request; auto url = String(request.URL.absoluteString.UTF8String); - auto message = Message {url}; - auto seq = message.seq; + auto message = Message(url, true); + message.isHTTP = true; if (String(request.HTTPMethod.UTF8String) == "OPTIONS") { auto headers = [NSMutableDictionary dictionary]; @@ -2599,20 +2599,10 @@ static void registerSchemeHandler (Router *router) { body = (char *) data; } - auto invoked = self.router->invoke(url, body, bufsize, [=](auto result) { - auto json = result.str(); - auto size = result.post.body != nullptr ? result.post.length : json.size(); - auto body = result.post.body != nullptr ? result.post.body : json.c_str(); - auto data = [NSData dataWithBytes: body length: size]; - auto headers = [NSMutableDictionary dictionary]; - + auto invoked = self.router->invoke(message, body, bufsize, [=](Result result) { + auto headers = [NSMutableDictionary dictionary]; headers[@"access-control-allow-origin"] = @"*"; headers[@"access-control-allow-methods"] = @"*"; - headers[@"content-length"] = [@(size) stringValue]; - - for (const auto& header : result.headers.entries) { - headers[@(header.key.c_str())] = @(header.value.c_str()); - } for (const auto& header : result.headers.entries) { auto key = [NSString stringWithUTF8String: trim(header.key).c_str()]; @@ -2620,6 +2610,50 @@ static void registerSchemeHandler (Router *router) { headers[key] = value; } + NSData* data = nullptr; + if (result.post.event_stream != nullptr) { + *result.post.event_stream = [task](const char* name, const char* data, + bool finished) { + auto event = + [NSString stringWithFormat:@"event: %@\ndata: %@\n\n", + [NSString stringWithUTF8String:name], + [NSString stringWithUTF8String:data]]; + + [task didReceiveData:[event dataUsingEncoding:NSUTF8StringEncoding]]; + if (finished) { + [task didFinish]; + } + return true; + }; + headers[@"content-type"] = @"text/event-stream"; + headers[@"cache-control"] = @"no-store"; + } else if (result.post.chunk_stream != nullptr) { + *result.post.chunk_stream = [task](const char* chunk, size_t chunk_size, + bool finished) { + [task didReceiveData:[NSData dataWithBytes:chunk length:chunk_size]]; + if (finished) { + [task didFinish]; + } + return true; + }; + headers[@"transfer-encoding"] = @"chunked"; + } else { + std::string json; + const char* body; + size_t size; + if (result.post.body != nullptr) { + body = result.post.body; + size = result.post.length; + } else { + json = result.str(); + body = json.c_str(); + size = json.size(); + headers[@"content-type"] = @"application/json"; + } + headers[@"content-length"] = @(size).stringValue; + data = [NSData dataWithBytes: body length: size]; + } + auto response = [[NSHTTPURLResponse alloc] initWithURL: task.request.URL statusCode: 200 @@ -2628,8 +2662,10 @@ static void registerSchemeHandler (Router *router) { ]; [task didReceiveResponse: response]; - [task didReceiveData: data]; - [task didFinish]; + if (data != nullptr) { + [task didReceiveData: data]; + [task didFinish]; + } #if !__has_feature(objc_arc) [response release]; @@ -3341,6 +3377,15 @@ namespace SSC::IPC { ResultCallback callback ) { auto message = Message { uri }; + return this->invoke(message, bytes, size, callback); + } + + bool Router::invoke ( + const Message& message, + const char *bytes, + size_t size, + ResultCallback callback + ) { auto name = message.name; MessageCallbackContext ctx; diff --git a/src/ipc/ipc.cc b/src/ipc/ipc.cc index 7656275c2a..7e96f23786 100644 --- a/src/ipc/ipc.cc +++ b/src/ipc/ipc.cc @@ -32,6 +32,7 @@ namespace SSC::IPC { this->seq = message.seq; this->uri = message.uri; this->args = message.args; + this->isHTTP = message.isHTTP; } Message::Message (const String& source, char *bytes, size_t size) diff --git a/src/ipc/ipc.hh b/src/ipc/ipc.hh index a62616acdc..b4fcbe6550 100644 --- a/src/ipc/ipc.hh +++ b/src/ipc/ipc.hh @@ -146,6 +146,7 @@ namespace SSC::IPC { int index = -1; Seq seq = ""; Map args; + bool isHTTP = false; Message () = default; Message (const Message& message); @@ -276,6 +277,12 @@ namespace SSC::IPC { size_t size, ResultCallback callback ); + bool invoke ( + const Message& msg, + const char *bytes, + size_t size, + ResultCallback callback + ); }; class Bridge { From 697e7e27685cd0a309d1bdd3718c298fc00c4acc Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:48:13 -0400 Subject: [PATCH 165/256] fix: support empty event name/data --- src/ipc/bridge.cc | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ipc/bridge.cc b/src/ipc/bridge.cc index ac286c4f3d..b7318594b0 100644 --- a/src/ipc/bridge.cc +++ b/src/ipc/bridge.cc @@ -2614,10 +2614,15 @@ static void registerSchemeHandler (Router *router) { if (result.post.event_stream != nullptr) { *result.post.event_stream = [task](const char* name, const char* data, bool finished) { + auto event_name = [NSString stringWithUTF8String:name]; + auto event_data = [NSString stringWithUTF8String:data]; auto event = - [NSString stringWithFormat:@"event: %@\ndata: %@\n\n", - [NSString stringWithUTF8String:name], - [NSString stringWithUTF8String:data]]; + event_name.length > 0 && event_data.length > 0 + ? [NSString stringWithFormat:@"event: %@\ndata: %@\n\n", + event_name, event_data] + : event_data.length > 0 + ? [NSString stringWithFormat:@"data: %@\n\n", event_data] + : [NSString stringWithFormat:@"event: %@\n\n", event_name]; [task didReceiveData:[event dataUsingEncoding:NSUTF8StringEncoding]]; if (finished) { From 6b69f65685bbe48b0346ceeedc7c41c085d7788d Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:56:35 -0400 Subject: [PATCH 166/256] fix(ipc): always decode url values in Router::invoke --- src/ipc/bridge.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ipc/bridge.cc b/src/ipc/bridge.cc index b7318594b0..dd47834306 100644 --- a/src/ipc/bridge.cc +++ b/src/ipc/bridge.cc @@ -3381,7 +3381,7 @@ namespace SSC::IPC { size_t size, ResultCallback callback ) { - auto message = Message { uri }; + auto message = Message(uri, true); return this->invoke(message, bytes, size, callback); } From 224ae37a9bfc6801e1c7c29d0827c7c28d8cae35 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 10 Oct 2023 18:32:27 -0400 Subject: [PATCH 167/256] fix: set isHTTP to true in Windows ipc scheme handler --- src/window/win.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/window/win.cc b/src/window/win.cc index bea8ceec55..d99ececc01 100644 --- a/src/window/win.cc +++ b/src/window/win.cc @@ -853,6 +853,7 @@ namespace SSC { DWORD actual; HRESULT r; auto msg = IPC::Message(uri); + msg.isHTTP = true; // TODO(trevnorris): Make sure index and seq are set. if (w->bridge->router.hasMappedBuffer(msg.index, msg.seq)) { IPC::MessageBuffer buf = w->bridge->router.getMappedBuffer(msg.index, msg.seq); From 32b8727101f893ebeba8ac0d4de12586d4b8020c Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 10 Oct 2023 18:34:48 -0400 Subject: [PATCH 168/256] chore: add platform support to descriptions --- include/socket/extension.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/include/socket/extension.h b/include/socket/extension.h index 31d569a066..7283d43d63 100644 --- a/include/socket/extension.h +++ b/include/socket/extension.h @@ -1148,6 +1148,8 @@ extern "C" { * * โš ๏ธ You must call `sapi_ipc_reply` before calling this function. * + * Supported on iOS/macOS only. + * * @param result - An IPC request result * @param chunk - The chunk to write * @param chunk_size - The size of the chunk @@ -1172,6 +1174,8 @@ extern "C" { * โš ๏ธ The `name` and `data` arguments must be null-terminated strings. Either * can be empty as long as it's null-terminated and the other is not empty. * + * Supported on iOS/macOS only. + * * @param result - An IPC request result * @param name - The event name * @param data - The event data From 94f37d1b4c9e314cc759a507ac18baf31eda4cbb Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Wed, 11 Oct 2023 22:47:27 -0400 Subject: [PATCH 169/256] fix(types): extend Window type with built-in events --- api/global.d.ts | 22 ++++++++++++++++++++++ api/index.d.ts | 22 ++++++++++++++++++++++ bin/generate-typescript-typings.sh | 2 +- bin/publish-npm-modules.sh | 1 + 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 api/global.d.ts diff --git a/api/global.d.ts b/api/global.d.ts new file mode 100644 index 0000000000..8d477449a7 --- /dev/null +++ b/api/global.d.ts @@ -0,0 +1,22 @@ +type MenuItemSelection = { + title: string + parent: string + state: '0' +} +declare interface Window { + addEventListener( + type: "menuItemSelected", + listener: (event: CustomEvent<MenuItemSelection>) => void, + options?: boolean | AddEventListenerOptions + ): void; + addEventListener( + type: "process-error", + listener: (event: CustomEvent<string>) => void, + options?: boolean | AddEventListenerOptions + ): void; + addEventListener( + type: "backend-exit", + listener: (event: CustomEvent<string>) => void, + options?: boolean | AddEventListenerOptions + ): void; +} \ No newline at end of file diff --git a/api/index.d.ts b/api/index.d.ts index 7578613913..9eb5f1d9ff 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -7123,3 +7123,25 @@ declare module "socket:test/harness" { import * as exports from "socket:test/harness"; } +type MenuItemSelection = { + title: string + parent: string + state: '0' +} +declare interface Window { + addEventListener( + type: "menuItemSelected", + listener: (event: CustomEvent<MenuItemSelection>) => void, + options?: boolean | AddEventListenerOptions + ): void; + addEventListener( + type: "process-error", + listener: (event: CustomEvent<string>) => void, + options?: boolean | AddEventListenerOptions + ): void; + addEventListener( + type: "backend-exit", + listener: (event: CustomEvent<string>) => void, + options?: boolean | AddEventListenerOptions + ): void; +} \ No newline at end of file diff --git a/bin/generate-typescript-typings.sh b/bin/generate-typescript-typings.sh index 81d7520f2d..ec1b2cd330 100755 --- a/bin/generate-typescript-typings.sh +++ b/bin/generate-typescript-typings.sh @@ -6,7 +6,7 @@ cd "$root" || exit $? "$root/node_modules/.bin/tsc" --emitDeclarationOnly --module es2022 --outFile api/index.tmp || exit $? -cat api/index.tmp.d.ts \ +cat api/index.tmp.d.ts api/global.d.ts \ | sed 's/declare module "\(.*\)"/declare module "socket:\1"/g' \ | sed 's/from "\(.*\)"/from "socket:\1"/g' \ | sed 's/import("\(.*\)")/import("socket:\1")/g' \ diff --git a/bin/publish-npm-modules.sh b/bin/publish-npm-modules.sh index 9f181bc462..a1ad05c1cc 100755 --- a/bin/publish-npm-modules.sh +++ b/bin/publish-npm-modules.sh @@ -149,6 +149,7 @@ if (( !only_platforms || only_top_level )); then cp -f "$root/LICENSE.txt" "$SOCKET_HOME/packages/$package" cp -f "$root/README.md" "$SOCKET_HOME/packages/$package/README-RUNTIME.md" cp -rf "$root/api"/* "$SOCKET_HOME/packages/$package" + rm "$SOCKET_HOME/packages/$package/global.d.ts" fi if (( !only_top_level )); then From f975c846a92cfc9e5e2647ee687f7639764fe6be Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Wed, 11 Oct 2023 21:13:01 -0400 Subject: [PATCH 170/256] fix: setSystemMenu JS validation --- api/application.js | 55 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/api/application.js b/api/application.js index 3c02965388..601e0e42b2 100644 --- a/api/application.js +++ b/api/application.js @@ -271,25 +271,48 @@ export async function setSystemMenu (o) { const frame = e.stack.split('\n')[2] const callerLineNo = frame.split(':').reverse()[1] + // Use this link to test the regex (https://regexr.com/7lhqe) + const validLineRegex = /^(?:([^:]+)|(.+)[:][ ]*((?:[+\w]+(?:[ ]+|[ ]*$))*)(.*))$/m + const validModifiers = /^(Alt|CommandOrControl|Control|Meta)$/ + for (let i = 0; i < lines.length; i++) { - const line = lines[i] - const l = Number(callerLineNo) + i - - let errMsg - - if (line.trim().length === 0) continue - if (/.*:\n/.test(line)) continue // ignore submenu labels - if (/---/.test(line)) continue // ignore separators - if (/\w+/.test(line) && !line.includes(':')) { - errMsg = 'Missing label' - } else if (/:\s*\+/.test(line)) { - errMsg = 'Missing accelerator' - } else if (/\+(\n|$)/.test(line)) { - errMsg = 'Missing modifier' + const lineText = lines[i].trim() + if (lineText.length === 0) { + continue // Empty line + } + if (lineText[0] === ';') { + continue // End of submenu + } + + let err + + const match = lineText.match(validLineRegex) + if (!match) { + err = 'Unsupported syntax' + } else { + const label = match[1] || match[2] + if (label.startsWith('---')) { + continue // Valid separator + } + const binding = match[3] + if (binding) { + const [accelerator, ...modifiers] = binding.split(/ *\+ */) + if (validModifiers.test(accelerator)) { + err = 'Missing accelerator' + } else { + for (const modifier of modifiers) { + if (!validModifiers.test(modifier)) { + err = `Invalid modifier "${modifier}"` + break + } + } + } + } } - if (errMsg) { - throw new Error(`${errMsg} on line ${l}: "${line}"`) + if (err) { + const lineNo = Number(callerLineNo) + i + throw new Error(`${err} on line ${lineNo}: "${lineText}"`) } } From 8f44773d19f35735be6ea3305577a1b8bc8030f3 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Wed, 11 Oct 2023 21:19:02 -0400 Subject: [PATCH 171/256] chore(fix): disallow leading `:` in setSystemMenu --- api/application.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/application.js b/api/application.js index 601e0e42b2..aec415bcda 100644 --- a/api/application.js +++ b/api/application.js @@ -295,7 +295,9 @@ export async function setSystemMenu (o) { continue // Valid separator } const binding = match[3] - if (binding) { + if (label.length === 0) { + err = 'Missing label' + } else if (binding) { const [accelerator, ...modifiers] = binding.split(/ *\+ */) if (validModifiers.test(accelerator)) { err = 'Missing accelerator' From 606b442b07d2b543cc641e6c709109fec856fa30 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Thu, 12 Oct 2023 12:39:31 -0400 Subject: [PATCH 172/256] chore(fix): prevent ":" in labels --- api/application.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/application.js b/api/application.js index aec415bcda..167995fdbe 100644 --- a/api/application.js +++ b/api/application.js @@ -297,6 +297,8 @@ export async function setSystemMenu (o) { const binding = match[3] if (label.length === 0) { err = 'Missing label' + } else if (label.includes(':')) { + err = 'Invalid label contains ":"' } else if (binding) { const [accelerator, ...modifiers] = binding.split(/ *\+ */) if (validModifiers.test(accelerator)) { From f1014d5aaee14d8eb840181ee1625ccce08ae4eb Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Thu, 12 Oct 2023 13:37:50 -0400 Subject: [PATCH 173/256] chore: update api readme --- api/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/api/README.md b/api/README.md index 73abc4c2fa..c3ef17e626 100644 --- a/api/README.md +++ b/api/README.md @@ -180,7 +180,7 @@ Set the native menu for the app. | :--- | :--- | :--- | | Not specified | Promise<ipc.Result> | | -## [`setSystemMenuItemEnabled(value)`](https://github.com/socketsupply/socket/blob/master/api/application.js#L304) +## [`setSystemMenuItemEnabled(value)`](https://github.com/socketsupply/socket/blob/master/api/application.js#L331) Set the enabled state of the system menu. @@ -192,23 +192,23 @@ Set the enabled state of the system menu. | :--- | :--- | :--- | | Not specified | Promise<ipc.Result> | | -## [runtimeVersion](https://github.com/socketsupply/socket/blob/master/api/application.js#L312) +## [runtimeVersion](https://github.com/socketsupply/socket/blob/master/api/application.js#L339) Socket Runtime version. -## [debug](https://github.com/socketsupply/socket/blob/master/api/application.js#L318) +## [debug](https://github.com/socketsupply/socket/blob/master/api/application.js#L345) Runtime debug flag. -## [config](https://github.com/socketsupply/socket/blob/master/api/application.js#L324) +## [config](https://github.com/socketsupply/socket/blob/master/api/application.js#L351) Application configuration. -## [backend](https://github.com/socketsupply/socket/blob/master/api/application.js#L329) +## [backend](https://github.com/socketsupply/socket/blob/master/api/application.js#L356) The application's backend instance. -### [`open(opts)`](https://github.com/socketsupply/socket/blob/master/api/application.js#L335) +### [`open(opts)`](https://github.com/socketsupply/socket/blob/master/api/application.js#L362) @@ -221,7 +221,7 @@ The application's backend instance. | :--- | :--- | :--- | | Not specified | Promise<ipc.Result> | | -### [`close()`](https://github.com/socketsupply/socket/blob/master/api/application.js#L343) +### [`close()`](https://github.com/socketsupply/socket/blob/master/api/application.js#L370) From 9b8c19ea592527647859ee01acf5dd467eb2f88f Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 22:25:06 -0400 Subject: [PATCH 174/256] chore(src/config.hh): move to 'src/core/config.hh' --- src/{ => core}/config.hh | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) rename src/{ => core}/config.hh (74%) diff --git a/src/config.hh b/src/core/config.hh similarity index 74% rename from src/config.hh rename to src/core/config.hh index e86988d708..890d29f44b 100644 --- a/src/config.hh +++ b/src/core/config.hh @@ -1,5 +1,5 @@ -#ifndef SSC_CONFIG_H -#define SSC_CONFIG_H +#ifndef SSC_CORE_CONFIG_H +#define SSC_CORE_CONFIG_H // TODO(@jwerle): remove this and any need for it #ifndef SSC_SETTINGS @@ -14,6 +14,11 @@ #define SSC_VERSION_HASH "" #endif +// TODO(@jwerle): use a better name +#if !defined(WAS_CODESIGNED) +#define WAS_CODESIGNED 0 +#endif + // TODO(@jwerle): stop using this and prefer a namespaced macro #ifndef DEBUG #define DEBUG 0 @@ -30,12 +35,11 @@ #endif #if defined(__cplusplus) -#include <map> -#include <string> +#include "types.hh" namespace SSC { - // from init.cc - extern const std::map<std::string, std::string> getUserConfig (); + // implemented in `init.cc` + extern const Map getUserConfig (); extern bool isDebugEnabled (); extern const char* getDevHost (); extern int getDevPort (); From 1b024a59d9cff6653cc35cf025dc944ee903750b Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 22:26:34 -0400 Subject: [PATCH 175/256] refactor(core): breakout into seperate concerns, move impl to units --- src/core/codec.cc | 259 ++++++++++++++++++++++++++++++++ src/core/codec.hh | 16 ++ src/core/core.cc | 25 +++ src/core/core.hh | 53 ++----- src/core/debug.hh | 76 ++++++++++ src/core/env.cc | 105 +++++++++++++ src/core/env.hh | 21 +++ src/core/file_system_watcher.hh | 5 +- src/core/ini.cc | 77 ++++++++++ src/core/ini.hh | 10 ++ src/core/io.cc | 27 ++++ src/core/io.hh | 10 ++ src/core/json.cc | 8 + src/core/json.hh | 103 ++++++------- src/core/platform.cc | 71 +++++++++ src/core/platform.hh | 121 +++++++++++++++ src/core/preload.cc | 2 + src/core/string.cc | 119 +++++++++++++++ src/core/string.hh | 34 +++++ src/core/types.hh | 43 ++++++ src/core/version.hh | 14 ++ 21 files changed, 1100 insertions(+), 99 deletions(-) create mode 100644 src/core/codec.cc create mode 100644 src/core/codec.hh create mode 100644 src/core/debug.hh create mode 100644 src/core/env.cc create mode 100644 src/core/env.hh create mode 100644 src/core/ini.cc create mode 100644 src/core/ini.hh create mode 100644 src/core/io.cc create mode 100644 src/core/io.hh create mode 100644 src/core/platform.cc create mode 100644 src/core/platform.hh create mode 100644 src/core/string.cc create mode 100644 src/core/string.hh create mode 100644 src/core/types.hh create mode 100644 src/core/version.hh diff --git a/src/core/codec.cc b/src/core/codec.cc new file mode 100644 index 0000000000..e3d9d8ffd8 --- /dev/null +++ b/src/core/codec.cc @@ -0,0 +1,259 @@ +#include "codec.hh" +#include "string.hh" + +#define UNSIGNED_IN_RANGE(value, min, max) ( \ + (unsigned char) (value) >= (unsigned char) (min) && \ + (unsigned char) (value) <= (unsigned char) (max) \ +) + +static const char DEC2HEX[16 + 1] = "0123456789ABCDEF"; + +// +// All ipc uses a URI schema, so all ipc data needs to be +// encoded as a URI component. This prevents escaping the +// protocol. +// +static const signed char HEX2DEC[256] = { + /* 0 1 2 3 4 5 6 7 8 9 A B C D E F */ + /* 0 */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + /* 1 */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + /* 2 */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + /* 3 */ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,-1,-1, -1,-1,-1,-1, + + /* 4 */ -1,10,11,12, 13,14,15,-1, -1,-1,-1,-1, -1,-1,-1,-1, + /* 5 */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + /* 6 */ -1,10,11,12, 13,14,15,-1, -1,-1,-1,-1, -1,-1,-1,-1, + /* 7 */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + + /* 8 */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + /* 9 */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + /* A */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + /* B */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + + /* C */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + /* D */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + /* E */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, + /* F */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1 +}; + +static const char SAFE[256] = { + /* 0 1 2 3 4 5 6 7 8 9 A B C D E F */ + /* 0 */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, + /* 1 */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, + /* 2 */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, + /* 3 */ 1,1,1,1, 1,1,1,1, 1,1,0,0, 0,0,0,0, + + /* 4 */ 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1, + /* 5 */ 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0, + /* 6 */ 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1, + /* 7 */ 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0, + + /* 8 */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, + /* 9 */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, + /* A */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, + /* B */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, + + /* C */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, + /* D */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, + /* E */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, + /* F */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0 +}; + +namespace SSC { + const Array<uint8_t, 8> toBytes (const uint64_t input) { + Array<uint8_t, 8> bytes; + // big endian, network order + bytes[0] = input >> 8*7; + bytes[1] = input >> 8*6; + bytes[2] = input >> 8*5; + bytes[3] = input >> 8*4; + bytes[4] = input >> 8*3; + bytes[5] = input >> 8*2; + bytes[6] = input >> 8*1; + bytes[7] = input >> 8*0; + return bytes; + } + + String encodeURIComponent (const String& input) { + auto pointer = (unsigned char*) input.c_str(); + const auto length = (int) input.length(); + auto const start = new unsigned char[length* 3]; + auto end = start; + const unsigned char* const maxLength = pointer + length; + + for (; pointer < maxLength; ++pointer) { + if (SAFE[*pointer]) { + *end++ = *pointer; + } else { + // escape this char + *end++ = '%'; + *end++ = DEC2HEX[*pointer >> 4]; + *end++ = DEC2HEX[*pointer & 0x0F]; + } + } + + String result((char*) start, (char*) end); + delete [] start; + return result; + } + + String decodeURIComponent (const String& input) { + // Note from RFC1630: "Sequences which start with a percent sign + // but are not followed by two hexadecimal characters (0-9, A-F) are reserved + // for future extension" + + const auto string = replace(input, "\\+", " "); + auto pointer = (const unsigned char *) string.c_str(); + const auto length = (int) string.length(); + const auto maxLength = pointer + length; + + char* const start = new char[length]; + char* end = start; + + while (pointer < maxLength - 2) { + if (*pointer == '%') { + char dec1, dec2; + if (-1 != (dec1 = HEX2DEC[*(pointer + 1)]) + && -1 != (dec2 = HEX2DEC[*(pointer + 2)])) { + + *end++ = (dec1 << 4) + dec2; + pointer += 3; + continue; + } + } + + *end++ = *pointer++; + } + + // the last 2- chars + while (pointer < maxLength) { + *end++ = *pointer++; + } + + String result(start, end); + delete [] start; + return result; + } + + String encodeHexString (const String& input) { + String output; + auto length = 2 * input.length(); + + output.reserve(length); + + for (unsigned char character : input) { + output.push_back(DEC2HEX[character >> 4]); + output.push_back(DEC2HEX[character & 15]); + } + + return output; + } + + String decodeHexString (const String& input) { + const auto length = input.length() / 2; + String output; + + output.reserve(length); + + for (auto character = input.begin(); character != input.end();) { + const int hi = HEX2DEC[(unsigned char) *character++]; + const int lo = HEX2DEC[(unsigned char) *character++]; + output.push_back(hi << 4 | lo); + } + + return output; + } + + size_t decodeUTF8 (char *output, const char *input, size_t length) { + unsigned char cp = 0; // code point + unsigned char lower = 0x80; + unsigned char upper = 0xBF; + + int x = 0; // cp needed + int y = 0; // cp seen + int size = 0; // output size + + for (int i = 0; i < length; ++i) { + auto b = (unsigned char) input[i]; + + if (b == 0) { + output[size++] = 0; + continue; + } + + if (x == 0) { + // 1 byte + if (UNSIGNED_IN_RANGE(b, 0x00, 0x7F)) { + output[size++] = b; + continue; + } + + if (!UNSIGNED_IN_RANGE(b, 0xC2, 0xF4)) { + break; + } + + // 2 byte + if (UNSIGNED_IN_RANGE(b, 0xC2, 0xDF)) { + x = 1; + cp = b - 0xC0; + } + + // 3 byte + if (UNSIGNED_IN_RANGE(b, 0xE0, 0xEF)) { + if (b == 0xE0) { + lower = 0xA0; + } else if (b == 0xED) { + upper = 0x9F; + } + + x = 2; + cp = b - 0xE0; + } + + // 4 byte + if (UNSIGNED_IN_RANGE(b, 0xF0, 0xF4)) { + if (b == 0xF0) { + lower = 0x90; + } else if (b == 0xF4) { + upper = 0x8F; + } + + x = 3; + cp = b - 0xF0; + } + + cp = cp * pow(64, x); + continue; + } + + if (!UNSIGNED_IN_RANGE(b, lower, upper)) { + lower = 0x80; + upper = 0xBF; + + // revert + cp = 0; + x = 0; + y = 0; + i--; + continue; + } + + lower = 0x80; + upper = 0xBF; + y++; + cp += (b - 0x80) * pow(64, x - y); + + if (y != x) { + continue; + } + + output[size++] = cp; + // continue to next + cp = 0; + x = 0; + y = 0; + } + + return size; + } +} diff --git a/src/core/codec.hh b/src/core/codec.hh new file mode 100644 index 0000000000..7652993bc2 --- /dev/null +++ b/src/core/codec.hh @@ -0,0 +1,16 @@ +#ifndef SSC_CORE_CODEC_HH +#define SSC_CORE_CODEC_HH + +#include "types.hh" + +namespace SSC { + String encodeURIComponent (const String& input); + String decodeURIComponent (const String& input); + String encodeHexString (const String& input); + String decodeHexString (const String& input); + size_t decodeUTF8 (char *output, const char *input, size_t length); + + const Array<uint8_t, 8> toBytes (const uint64_t input); +} + +#endif diff --git a/src/core/core.cc b/src/core/core.cc index 5b22f8c912..793a9e809e 100644 --- a/src/core/core.cc +++ b/src/core/core.cc @@ -1,6 +1,31 @@ #include "core.hh" +#define IMAX_BITS(m) ((m)/((m) % 255+1) / 255 % 255 * 8 + 7-86 / ((m) % 255+12)) +#define RAND_MAX_WIDTH IMAX_BITS(RAND_MAX) + namespace SSC { + uint64_t rand64 () { + uint64_t r = 0; + static bool init = false; + + if (!init) { + init = true; + srand(time(0)); + } + + for (int i = 0; i < 64; i += RAND_MAX_WIDTH) { + r <<= RAND_MAX_WIDTH; + r ^= (unsigned) rand(); + } + return r; + } + + + void msleep (uint64_t ms) { + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); + } + Headers::Header::Header (const Header& header) { this->key = header.key; this->value = header.value; diff --git a/src/core/core.hh b/src/core/core.hh index f8f5659cc9..738dbadccc 100644 --- a/src/core/core.hh +++ b/src/core/core.hh @@ -1,47 +1,19 @@ #ifndef SSC_CORE_CORE_H #define SSC_CORE_CORE_H -#include "../common.hh" -#include <uv.h> - -#if defined(__APPLE__) -#import <Webkit/Webkit.h> -#import <Network/Network.h> -#import <Foundation/Foundation.h> -#import <CoreLocation/CoreLocation.h> -#import <CoreBluetooth/CoreBluetooth.h> -#import <UserNotifications/UserNotifications.h> -#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h> - -#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR -#import <UIKit/UIKit.h> -#else -#import <Cocoa/Cocoa.h> -#endif -#elif defined(__linux__) && !defined(__ANDROID__) -#include <JavaScriptCore/JavaScript.h> -#include <webkit2/webkit2.h> -#include <gtk/gtk.h> -#elif defined(_WIN32) -#include <WebView2.h> -#include <WebView2EnvironmentOptions.h> -#include <shellapi.h> - -#pragma comment(lib, "advapi32.lib") -#pragma comment(lib, "shell32.lib") -#pragma comment(lib, "version.lib") -#pragma comment(lib, "user32.lib") -#pragma comment(lib, "WebView2LoaderStatic.lib") -#pragma comment(lib, "Ws2_32.lib") -#pragma comment(lib, "iphlpapi.lib") -#pragma comment(lib, "psapi.lib") -#pragma comment(lib, "userenv.lib") -#pragma comment(lib, "libuv.lib") -#pragma comment(lib, "dbghelp.lib") -#endif - +#include "codec.hh" +#include "config.hh" +#include "debug.hh" +#include "env.hh" +#include "file_system_watcher.hh" +#include "ini.hh" +#include "io.hh" #include "json.hh" +#include "platform.hh" #include "preload.hh" +#include "string.hh" +#include "types.hh" +#include "version.hh" #if defined(__APPLE__) @interface SSCBluetoothController : NSObject< @@ -65,6 +37,9 @@ namespace SSC { constexpr int EVENT_LOOP_POLL_TIMEOUT = 32; // in milliseconds + uint64_t rand64 (); + void msleep (uint64_t ms); + // forward class Core; diff --git a/src/core/debug.hh b/src/core/debug.hh new file mode 100644 index 0000000000..d588a18947 --- /dev/null +++ b/src/core/debug.hh @@ -0,0 +1,76 @@ +#ifndef SSC_CORE_DEBUG_H +#define SSC_CORE_DEBUG_H + +#include "config.hh" +#include "platform.hh" + +#if defined(__APPLE__) +#include <TargetConditionals.h> +#include <OSLog/OSLog.h> + +// Apple +#ifndef debug +// define `ssc_os_debug` (macos/ios) +#if defined(SSC_CLI) +# define ssc_os_debug(...) +#else +static os_log_t SSC_OS_LOG_DEBUG_BUNDLE = nullptr; +// wrap `os_log*` functions for global debugger +#define ssc_os_debug(format, fmt, ...) ({ \ + if (!SSC_OS_LOG_DEBUG_BUNDLE) { \ + static auto userConfig = SSC::getUserConfig(); \ + static auto bundleIdentifier = userConfig["meta_bundle_identifier"]; \ + SSC_OS_LOG_DEBUG_BUNDLE = os_log_create( \ + bundleIdentifier.c_str(), \ + "socket.runtime.debug" \ + ); \ + } \ + \ + auto string = [NSString stringWithFormat: @fmt, ##__VA_ARGS__]; \ + os_log_error( \ + SSC_OS_LOG_DEBUG_BUNDLE, \ + "%{public}s", \ + string.UTF8String \ + ); \ +}) +#endif + +// define `debug(...)` macro +#define debug(format, ...) ({ \ + NSLog(@format, ##__VA_ARGS__); \ + ssc_os_debug("%{public}@", format, ##__VA_ARGS__); \ +}) +#endif // `debug` +#endif // `__APPLE__` + +// Linux +#if defined(__linux__) && !defined(__ANDROID__) +# ifndef debug +# define debug(format, ...) fprintf(stderr, format "\n", ##__VA_ARGS__) +# endif // `debug` +#endif // `__linux__` + +// Android (Linux) +#if defined(__linux__) && defined(__ANDROID__) +# ifndef debug +# define debug(format, ...) \ + __android_log_print( \ + ANDROID_LOG_DEBUG, \ + "Console", \ + format, \ + ##__VA_ARGS__ \ + ); +# endif // `debug` +#endif // `__ANDROID__` + +// Windows +#if defined(_WIN32) +# if defined(_WIN32) && defined(DEBUG) +# define _WIN32_DEBUG 1 +# endif // `_WIN32 && DEBUG` +# ifndef debug +# define debug(format, ...) fprintf(stderr, format "\n", ##__VA_ARGS__) +# endif // `debug` +#endif // `_WIN32` + +#endif diff --git a/src/core/env.cc b/src/core/env.cc new file mode 100644 index 0000000000..159f760d00 --- /dev/null +++ b/src/core/env.cc @@ -0,0 +1,105 @@ +#include <stdlib.h> + +#include "config.hh" +#include "env.hh" +#include "string.hh" + +namespace SSC { + bool Env::has (const char* name) { + static auto userConfig = getUserConfig(); + + if (userConfig[String("env_") + name].size() > 0) { + return true; + } + + #if defined(_WIN32) + char* value = nullptr; + size_t size = 0; + auto result = _dupenv_s(&value, &size, name); + + if (value && value[0] == '\0') { + free(value); + return false; + } + + free(value); + + if (size == 0 || result != 0) { + return false; + } + #else + auto value = getenv(name); + + if (value == nullptr || value[0] == '\0') { + return false; + } + #endif + + return true; + } + + bool Env::has (const String& name) { + return has(name.c_str()); + } + + String Env::get (const char* name) { + static auto userConfig = getUserConfig(); + + if (userConfig[String("env_") + name].size() > 0) { + return userConfig[String("env_") + name]; + } + + #if defined(_WIN32) + char* variableValue = nullptr; + std::size_t valueSize = 0; + auto query = _dupenv_s(&variableValue, &valueSize, name); + + String result; + if (query == 0 && variableValue != nullptr && valueSize > 0) { + result.assign(variableValue, valueSize - 1); + free(variableValue); + } + + return result; + #else + auto v = getenv(name); + + if (v != nullptr) { + return String(v); + } + + return String(""); + #endif + } + + String Env::get (const String& name) { + return get(name.c_str()); + } + + String Env::get (const String& name, const String& fallback) { + const auto value = get(name); + + if (value.size() == 0) { + return fallback; + } + + return value; + } + + void Env::set (const String& name, const String& value) { + #if defined(_WIN32) + return _putenv((name + "=" + value).c_str()); + #else + setenv(name.c_str(), value.c_str(), 1); + #endif + } + + void Env::set (const char* name) { + #if defined(_WIN32) + return _putenv(name); + #endif + + auto parts = split(String(name), '='); + set(parts[0], parts[1]); + } +} diff --git a/src/core/env.hh b/src/core/env.hh new file mode 100644 index 0000000000..ac30dae014 --- /dev/null +++ b/src/core/env.hh @@ -0,0 +1,21 @@ +#ifndef SSC_CORE_ENV_H +#define SSC_CORE_ENV_H + +#include "types.hh" + +namespace SSC { + class Env { + public: + static bool has (const char* name); + static bool has (const String& name); + + static String get (const char* name); + static String get (const String& name); + static String get (const String& name, const String& fallback); + + static void set (const String& name, const String& value); + static void set (const char* name); + }; +} + +#endif diff --git a/src/core/file_system_watcher.hh b/src/core/file_system_watcher.hh index e4b447be42..c350670309 100644 --- a/src/core/file_system_watcher.hh +++ b/src/core/file_system_watcher.hh @@ -1,9 +1,8 @@ #ifndef SSC_FILE_SYSTEM_WATCHER #define SSC_FILE_SYSTEM_WATCHER -#include "core.hh" - -using AtomicBool = std::atomic<bool>; +#include "platform.hh" +#include "types.hh" namespace SSC { class FileSystemWatcher { diff --git a/src/core/ini.cc b/src/core/ini.cc new file mode 100644 index 0000000000..e035af3839 --- /dev/null +++ b/src/core/ini.cc @@ -0,0 +1,77 @@ +#include "ini.hh" + +namespace SSC::INI { + Map parse (const String& source) { + Vector<String> entries = split(source, '\n'); + String prefix = ""; + Map settings = {}; + + for (auto entry : entries) { + entry = trim(entry); + + // handle a variety of comment styles + if (entry[0] == ';' || entry[0] == '#') { + continue; + } + + if (entry.starts_with("[") && entry.ends_with("]")) { + if (entry.starts_with("[.") && entry.ends_with("]")) { + prefix += entry.substr(2, entry.length() - 3); + } else { + prefix = entry.substr(1, entry.length() - 2); + } + + prefix = replace(prefix, "\\.", "_"); + if (prefix.size() > 0) { + prefix += "_"; + } + + continue; + } + + auto index = entry.find_first_of('='); + + if (index >= 0 && index <= entry.size()) { + auto key = trim(prefix + entry.substr(0, index)); + auto value = trim(entry.substr(index + 1)); + + // trim quotes from quoted strings + size_t closing_quote_index = -1; + bool quoted_value = false; + if (value[0] == '"') { + closing_quote_index = value.find_first_of('"', 1); + if (closing_quote_index != std::string::npos) { + quoted_value = true; + value = trim(value.substr(1, closing_quote_index - 1)); + } + } + + if (!quoted_value) { + // ignore comments within quoted part of value + auto i = value.find_first_of(';'); + auto j = value.find_first_of('#'); + + if (i > 0) { + value = trim(value.substr(0, i)); + } + else if (j > 0) { + value = trim(value.substr(0, j)); + } + } + + if (key.ends_with("[]")) { + key = trim(key.substr(0, key.size() - 2)); + if (settings[key].size() > 0) { + settings[key] += " " + value; + } else { + settings[key] = value; + } + } else { + settings[key] = value; + } + } + } + + return settings; + } +} diff --git a/src/core/ini.hh b/src/core/ini.hh new file mode 100644 index 0000000000..48e00788e8 --- /dev/null +++ b/src/core/ini.hh @@ -0,0 +1,10 @@ +#ifndef SSC_CORE_INI_H +#define SSC_CORE_INI_H + +#include "string.hh" +#include "types.hh" + +namespace SSC::INI { + Map parse (const String& source); +} +#endif diff --git a/src/core/io.cc b/src/core/io.cc new file mode 100644 index 0000000000..36906c8360 --- /dev/null +++ b/src/core/io.cc @@ -0,0 +1,27 @@ +#include <iostream> + +#include "../cli/cli.hh" +#include "env.hh" +#include "io.hh" + +namespace SSC::IO { + void write (const String& input, bool isErrorOutput) { + static const auto GITHUB_ACTIONS_CI = Env::get("GITHUB_ACTIONS_CI"); + static const auto isGitHubActionsCI = GITHUB_ACTIONS_CI.size() > 0; + auto& stream = isErrorOutput ? std::cerr : std::cout; + + stream << input; + + // skip writing newline if running on Windows GHA CI + #if defined(_WIN32) + if (isGitHubActionsCI) { + CLI::notify(); + return; + } + + #endif + stream << std::endl; + + CLI::notify(); + } +} diff --git a/src/core/io.hh b/src/core/io.hh new file mode 100644 index 0000000000..1598f58ce6 --- /dev/null +++ b/src/core/io.hh @@ -0,0 +1,10 @@ +#ifndef SSC_CORE_IO_H +#define SSC_CORE_IO_H + +#include "types.hh" + +namespace SSC::IO { + void write (const String& input, bool isErrorOutput); +} + +#endif diff --git a/src/core/json.cc b/src/core/json.cc index 25da1e9412..4e99a3a4fa 100644 --- a/src/core/json.cc +++ b/src/core/json.cc @@ -1,4 +1,7 @@ +#include <regex> + #include "json.hh" +#include "string.hh" namespace SSC::JSON { Null null; @@ -73,6 +76,11 @@ namespace SSC::JSON { this->data = number.str(); } + SSC::String String::str () const { + auto escaped = replace(this->data, "\"", "\\\""); + return "\"" + replace(escaped, "\n", "\\n") + "\""; + } + Any::Any (const Null null) { this->pointer = std::shared_ptr<void>(new Null()); this->type = Type::Null; diff --git a/src/core/json.hh b/src/core/json.hh index bef9162229..671a2cf535 100644 --- a/src/core/json.hh +++ b/src/core/json.hh @@ -1,7 +1,7 @@ #ifndef SSC_SOCKET_JSON_HH #define SSC_SOCKET_JSON_HH -#include "../common.hh" +#include "types.hh" namespace SSC::JSON { // forward @@ -14,27 +14,19 @@ namespace SSC::JSON { class Number; class String; - using ObjectEntries = std::map<std::string, Any>; + using ObjectEntries = std::map<SSC::String, Any>; using ArrayEntries = std::vector<Any>; - inline auto replace ( - const std::string &source, - const std::string &pattern, - const std::string &value - ) { - return std::regex_replace(source, std::regex(pattern), value); - } - class Error : public std::invalid_argument { public: - std::string name; - std::string message; - std::string location; + SSC::String name; + SSC::String message; + SSC::String location; Error ( - const std::string& name, - const std::string& message, - const std::string& location + const SSC::String& name, + const SSC::String& message, + const SSC::String& location ) : std::invalid_argument(name + ": " + message + " (from " + location + ")") { this->name = name; this->message = message; @@ -42,7 +34,7 @@ namespace SSC::JSON { } auto str () const { - return std::string(name + ": " + message + " (from " + location + ")"); + return SSC::String(name + ": " + message + " (from " + location + ")"); } }; @@ -65,15 +57,15 @@ namespace SSC::JSON { auto typeof () const { switch (this->type) { - case Type::Empty: return std::string("empty"); - case Type::Raw: return std::string("raw"); - case Type::Any: return std::string("any"); - case Type::Array: return std::string("array"); - case Type::Boolean: return std::string("boolean"); - case Type::Number: return std::string("number"); - case Type::Null: return std::string("null"); - case Type::Object: return std::string("object"); - case Type::String: return std::string("string"); + case Type::Empty: return SSC::String("empty"); + case Type::Raw: return SSC::String("raw"); + case Type::Any: return SSC::String("any"); + case Type::Array: return SSC::String("array"); + case Type::Boolean: return SSC::String("boolean"); + case Type::Number: return SSC::String("number"); + case Type::Null: return SSC::String("null"); + case Type::Object: return SSC::String("object"); + case Type::String: return SSC::String("string"); } } @@ -95,7 +87,7 @@ namespace SSC::JSON { return nullptr; } - std::string str () const { + SSC::String str () const { return "null"; } }; @@ -141,7 +133,7 @@ namespace SSC::JSON { Any (const Number); Any (const char); Any (const char *); - Any (const std::string); + Any (const SSC::String); Any (const String); Any (const Object); Any (const ObjectEntries); @@ -149,7 +141,7 @@ namespace SSC::JSON { Any (const ArrayEntries); Any (const Raw source); - std::string str () const; + SSC::String str () const; template <typename T> T& as () const { auto ptr = this->pointer.get(); @@ -162,13 +154,13 @@ namespace SSC::JSON { } }; - class Raw : public Value<std::string, Type::Raw> { + class Raw : public Value<SSC::String, Type::Raw> { public: Raw (const Raw& raw) { this->data = raw.data; } Raw (const Raw* raw) { this->data = raw->data; } - Raw (const std::string& source) { this->data = source; } + Raw (const SSC::String& source) { this->data = source; } - const std::string str () const { + const SSC::String str () const { return this->data; } }; @@ -183,7 +175,7 @@ namespace SSC::JSON { public: using Entries = ObjectEntries; Object () = default; - Object (std::map<std::string, int> entries) { + Object (std::map<SSC::String, int> entries) { for (auto const &tuple : entries) { auto key = tuple.first; auto value = tuple.second; @@ -191,7 +183,7 @@ namespace SSC::JSON { } } - Object (std::map<std::string, bool> entries) { + Object (std::map<SSC::String, bool> entries) { for (auto const &tuple : entries) { auto key = tuple.first; auto value = tuple.second; @@ -199,7 +191,7 @@ namespace SSC::JSON { } } - Object (std::map<std::string, double> entries) { + Object (std::map<SSC::String, double> entries) { for (auto const &tuple : entries) { auto key = tuple.first; auto value = tuple.second; @@ -207,7 +199,7 @@ namespace SSC::JSON { } } - Object (std::map<std::string, int64_t> entries) { + Object (std::map<SSC::String, int64_t> entries) { for (auto const &tuple : entries) { auto key = tuple.first; auto value = tuple.second; @@ -227,7 +219,7 @@ namespace SSC::JSON { this->data = object.value(); } - Object (const std::map<std::string, std::string> map) { + Object (const std::map<SSC::String, SSC::String> map) { for (const auto& tuple : map) { auto key = tuple.first; auto value = Any(tuple.second); @@ -235,13 +227,13 @@ namespace SSC::JSON { } } - std::string str () const; + SSC::String str () const; const Object::Entries value () const { return this->data; } - Any& get (const std::string key) { + Any& get (const SSC::String key) { if (this->data.find(key) != this->data.end()) { return this->data.at(key); } @@ -249,15 +241,15 @@ namespace SSC::JSON { return anyNull; } - void set (const std::string key, Any value) { + void set (const SSC::String key, Any value) { this->data[key] = value; } - bool has (const std::string& key) const { + bool has (const SSC::String& key) const { return this->data.find(key) != this->data.end(); } - Any operator [] (const std::string& key) const { + Any operator [] (const SSC::String& key) const { if (this->data.find(key) != this->data.end()) { return this->data.at(key); } @@ -265,7 +257,7 @@ namespace SSC::JSON { return nullptr; } - Any &operator [] (const std::string& key) { + Any &operator [] (const SSC::String& key) { return this->data[key]; } @@ -288,7 +280,7 @@ namespace SSC::JSON { } } - std::string str () const; + SSC::String str () const; Array::Entries value () const { return this->data; @@ -376,7 +368,7 @@ namespace SSC::JSON { this->data = data != nullptr; } - Boolean (std::string string) { + Boolean (SSC::String string) { this->data = string.size() > 0; } @@ -384,7 +376,7 @@ namespace SSC::JSON { return this->data; } - std::string str () const { + SSC::String str () const { return this->data ? "true" : "false"; } }; @@ -422,26 +414,26 @@ namespace SSC::JSON { return this->data; } - std::string str () const; + SSC::String str () const; }; - class String : public Value<std::string, Type::String> { + class String : public Value<SSC::String, Type::String> { public: String () = default; String (const String& data) { - this->data = std::string(data.value()); + this->data = SSC::String(data.value()); } - String (const std::string data) { + String (const SSC::String data) { this->data = data; } String (const char data) { - this->data = std::string(1, data); + this->data = SSC::String(1, data); } String (const char *data) { - this->data = std::string(data); + this->data = SSC::String(data); } String (const Any& any) { @@ -454,12 +446,9 @@ namespace SSC::JSON { this->data = boolean.str(); } - std::string str () const { - auto escaped = replace(this->data, "\"", "\\\""); - return "\"" + replace(escaped, "\n", "\\n") + "\""; - } + SSC::String str () const; - std::string value () const { + SSC::String value () const { return this->data; } diff --git a/src/core/platform.cc b/src/core/platform.cc new file mode 100644 index 0000000000..fff2f167f4 --- /dev/null +++ b/src/core/platform.cc @@ -0,0 +1,71 @@ +#include "platform.hh" + +namespace SSC { + extern const RuntimePlatform platform = { + #if defined(__x86_64__) || defined(_M_X64) + .arch = "x86_64", + #elif defined(__aarch64__) || defined(_M_ARM64) + .arch = "arm64", + #elif defined(__i386__) && !defined(__ANDROID__) + # error Socket is not supported on i386. + #else + .arch = "unknown", + #endif + + // Windows + #if defined(_WIN32) + .os = "win32", + .win = true, + #endif + + // macOS & iOS + #if defined(__APPLE__) + #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + .os = "ios", + .ios = true, + #else + .os = "mac", + .mac = true, + #endif + #if defined(__unix__) || defined(unix) || defined(__unix) + .unix = true + #else + .unix = false + #endif + #endif + + // Android & Linux + #if defined(__linux__) + #undef linux + #ifdef __ANDROID__ + .os = "android", + #else + .os = "linux", + #endif + + .linux = true, + + #if defined(__unix__) || defined(unix) || defined(__unix) + .unix = true + #else + .unix = false + #endif + #endif + + // FreeBSD + #if defined(__FreeBSD__) + .os = "freebsd", + #if defined(__unix__) || defined(unix) || defined(__unix) + .unix = true + #else + .unix = false + #endif + #endif + + // OpenBSD (possibly) + #if !defined(__APPLE__) && defined(BSD) && (defined(__unix__) || defined(unix) || defined(__unix)) + .os = "openbsd", + .unix = true + #endif + }; +} diff --git a/src/core/platform.hh b/src/core/platform.hh new file mode 100644 index 0000000000..750477df87 --- /dev/null +++ b/src/core/platform.hh @@ -0,0 +1,121 @@ +#ifndef SSC_CORE_PLATFORM_H +#define SSC_CORE_PLATFORM_H + +// All Platforms +#include <errno.h> +#include <math.h> +#include <stdio.h> +#include <stdlib.h> +#include <uv.h> + +// Apple (macOS/iOS) +#if defined(__APPLE__) +#include <CoreLocation/CoreLocation.h> +#include <CoreBluetooth/CoreBluetooth.h> +#include <Foundation/Foundation.h> +#include <Network/Network.h> +#include <OSLog/OSLog.h> +#include <TargetConditionals.h> +#include <UserNotifications/UserNotifications.h> +#include <UniformTypeIdentifiers/UniformTypeIdentifiers.h> +#include <Webkit/Webkit.h> + +#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR +#include <_types/_uint64_t.h> +#include <netinet/in.h> +#include <sys/un.h> +#include <UIKit/UIKit.h> +#else +#include <Cocoa/Cocoa.h> +#include <objc/objc-runtime.h> +#endif +#endif // `__APPLE__` + +// Linux +#if defined(__linux__) && !defined(__ANDROID__) +#include <cstring> +#include <JavaScriptCore/JavaScript.h> +#include <gtk/gtk.h> +#include <webkit2/webkit2.h> +#endif // `__linux__` + +// Android (Linux) +#if defined(__ANDROID__) +// Java Native Interface +// @see https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html +#include <jni.h> +#include <android/asset_manager.h> +#include <android/asset_manager_jni.h> +#include <android/log.h> +#endif // `__ANDROID__` + +// Windows +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + +#undef _WINSOCKAPI_ +#define _WINSOCKAPI_ + +#include <WinSock2.h> +#include <windows.h> + +#include <dwmapi.h> +#include <io.h> +#include <tchar.h> +#include <wingdi.h> + +#include <signal.h> +#include <shlobj_core.h> +#include <shobjidl.h> + +#include <WebView2.h> +#include <WebView2EnvironmentOptions.h> +#include <shellapi.h> + +#pragma comment(lib, "advapi32.lib") +#pragma comment(lib, "shell32.lib") +#pragma comment(lib, "version.lib") +#pragma comment(lib, "user32.lib") +#pragma comment(lib, "WebView2LoaderStatic.lib") +#pragma comment(lib, "Ws2_32.lib") +#pragma comment(lib, "iphlpapi.lib") +#pragma comment(lib, "psapi.lib") +#pragma comment(lib, "userenv.lib") +#pragma comment(lib, "libuv.lib") +#pragma comment(lib, "dbghelp.lib") + +#define isatty _isatty +#define fileno _fileno +#endif // `_WIN32` + + +// POSIX[like] Platforms +#if !defined(_WIN32) +#include <arpa/inet.h> +#include <ifaddrs.h> +#include <sys/socket.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <unistd.h> +#endif + +#include "config.hh" +#include "types.hh" + +namespace SSC { + struct RuntimePlatform { + const String arch = ""; + const String os = ""; + bool mac = false; + bool ios = false; + bool win = false; + bool linux = false; + bool unix = false; + }; + + extern const RuntimePlatform platform; +} + +#endif diff --git a/src/core/preload.cc b/src/core/preload.cc index a6a711cdf7..fc844ea454 100644 --- a/src/core/preload.cc +++ b/src/core/preload.cc @@ -1,4 +1,6 @@ +#include "codec.hh" #include "preload.hh" +#include "string.hh" namespace SSC { String createPreload ( diff --git a/src/core/string.cc b/src/core/string.cc new file mode 100644 index 0000000000..1acb87b3ad --- /dev/null +++ b/src/core/string.cc @@ -0,0 +1,119 @@ +#include "string.hh" +#include <regex> + +namespace SSC { + String replace (const String& source, const String& regex, const String& value) { + return std::regex_replace(source, std::regex(regex), value); + } + + String tmpl (const String& source, const Map& variables) { + String output = source; + + for (const auto tuple : variables) { + auto key = String("[{]+(" + tuple.first + ")[}]+"); + auto value = tuple.second; + output = std::regex_replace(output, std::regex(key), value); + } + + return output; + } + + const Vector<String> split (const String& source, const char& character) { + String buffer; + Vector<String> result; + + for (const auto current : source) { + if (current != character) { + buffer += current; + } else if (current == character && buffer != "") { + result.push_back(buffer); + buffer = ""; + } + } + + if (!buffer.empty()) { + result.push_back(buffer); + } + + return result; + } + + const Vector<String> splitc (const String& source, const char& character) { + String buffer; + Vector<String> result; + + for (const auto current : source) { + if (current != character) { + buffer += current; + } else if (current == character) { + result.push_back(buffer); + buffer = ""; + } + } + + result.push_back(buffer); + + return result; + } + + String trim (String source) { + source.erase(0, source.find_first_not_of(" \r\n\t")); + source.erase(source.find_last_not_of(" \r\n\t") + 1); + return source; + } + + WString convertStringToWString (const String& source) { + WString result(source.length(), L' '); + std::copy(source.begin(), source.end(), result.begin()); + return result; + } + + WString convertStringToWString (const WString& source) { + return source; + } + + String convertWStringToString (const WString& source) { + String result(source.length(), ' '); + std::copy(source.begin(), source.end(), result.begin()); + return result; + } + + String convertWStringToString (const String& source) { + return source; + } + + const String join (const Vector<String>& vector, const String& separator) { + auto missing = vector.size(); + StringStream joined; + + for (const auto& item : vector) { + joined << item; + if (--missing > 0) { + joined << separator << " "; + } + } + + return trim(joined.str()); + } + + Vector<String> parseStringList (const String& string, const Vector<char>& separators) { + auto list = Vector<String>(); + for (const auto& separator : separators) { + for (const auto& part: split(string, separator)) { + if (std::find(list.begin(), list.end(), part) == list.end()) { + list.push_back(part); + } + } + } + + return list; + } + + Vector<String> parseStringList (const String& string, const char separator) { + return split(string, separator); + } + + Vector<String> parseStringList (const String& string) { + return parseStringList(string, { ' ', ',' }); + } +} diff --git a/src/core/string.hh b/src/core/string.hh new file mode 100644 index 0000000000..c80dcaee1a --- /dev/null +++ b/src/core/string.hh @@ -0,0 +1,34 @@ +#ifndef SSC_CORE_STRING_HH +#define SSC_CORE_STRING_HH + +#include "types.hh" + +/** + * Converts a literal expression to an inline string: + * `CONVERT_TO_STRING(this_value)` becomes "this_value" + */ +#define _CONVERT_TO_STRING(value) #value +#define CONVERT_TO_STRING(value) _CONVERT_TO_STRING(value) + +namespace SSC { + // transform + String replace (const String& source, const String& regex, const String& value); + String tmpl (const String& source, const Map& variables); + String trim (String source); + + // conversion + WString convertStringToWString (const String& source); + WString convertStringToWString (const WString& source); + String convertWStringToString (const WString& source); + String convertWStringToString (const String& source); + + // vector parsers + const Vector<String> split (const String& source, const char& character); + const Vector<String> splitc (const String& source, const char& character); + const String join (const Vector<String>& vector, const String& separator); + Vector<String> parseStringList (const String& string, const Vector<char>& separators); + Vector<String> parseStringList (const String& string, const char separator); + Vector<String> parseStringList (const String& string); +} + +#endif diff --git a/src/core/types.hh b/src/core/types.hh new file mode 100644 index 0000000000..79a63609ed --- /dev/null +++ b/src/core/types.hh @@ -0,0 +1,43 @@ +#ifndef SSC_CORE_TYPES_H +#define SSC_CORE_TYPES_H + +#include <array> +#include <filesystem> +#include <functional> +#include <map> +#include <mutex> +#include <queue> +#include <sstream> +#include <string> +#include <thread> +#include <vector> + +#if defined(__APPLE__) +#include <TargetConditionals.h> +#endif + +namespace SSC { +#if !defined(__APPLE__) || (defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR) + namespace fs = std::filesystem; + using Path = fs::path; +#endif + + using AtomicBool = std::atomic<bool>; + using String = std::string; + using StringStream = std::stringstream; + using WString = std::wstring; + using WStringStream = std::wstringstream; + using Map = std::map<String, String>; + using Mutex = std::recursive_mutex; + using Lock = std::lock_guard<Mutex>; + using Thread = std::thread; + + template <typename T, int k> using Array = std::array<T, k>; + template <typename T> using Queue = std::queue<T>; + template <typename T> using Vector = std::vector<T>; + + using ExitCallback = std::function<void(int code)>; + using MessageCallback = std::function<void(const String)>; +} + +#endif diff --git a/src/core/version.hh b/src/core/version.hh new file mode 100644 index 0000000000..287636700d --- /dev/null +++ b/src/core/version.hh @@ -0,0 +1,14 @@ +#ifndef SSC_CORE_VERSION +#define SSC_CORE_VERSION + +#include "config.hh" +#include "string.hh" +#include "types.hh" + +namespace SSC { + inline const auto VERSION_FULL_STRING = String(CONVERT_TO_STRING(SSC_VERSION) " (" CONVERT_TO_STRING(SSC_VERSION_HASH) ")"); + inline const auto VERSION_HASH_STRING = String(CONVERT_TO_STRING(SSC_VERSION_HASH)); + inline const auto VERSION_STRING = String(CONVERT_TO_STRING(SSC_VERSION)); +} + +#endif From a6680d639776a064f85cefcb2fc937d405c6a59c Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 22:27:07 -0400 Subject: [PATCH 176/256] refactor(ipc): refactor to use single 'core.hh' --- src/ipc/bridge.cc | 16 +++++++++------- src/ipc/ipc.cc | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/ipc/bridge.cc b/src/ipc/bridge.cc index dd47834306..8b28cca2ab 100644 --- a/src/ipc/bridge.cc +++ b/src/ipc/bridge.cc @@ -1,10 +1,12 @@ -#include "ipc.hh" -#include "../extension/extension.hh" +#include <regex> #if defined(__APPLE__) #include <UniformTypeIdentifiers/UniformTypeIdentifiers.h> #endif +#include "../extension/extension.hh" +#include "ipc.hh" + #define SOCKET_MODULE_CONTENT_TYPE "text/javascript" #define IPC_BINARY_CONTENT_TYPE "application/octet-stream" #define IPC_JSON_CONTENT_TYPE "text/json" @@ -1785,10 +1787,10 @@ static void initRouterTable (Router *router) { }, {"host-operating-system", #if defined(__APPLE__) - #if TARGET_OS_IPHONE - "iphoneos" - #elif TARGET_IPHONE_SIMULATOR + #if TARGET_IPHONE_SIMULATOR "iphonesimulator" + #elif TARGET_OS_IPHONE + "iphoneos" #else "macosx" #endif @@ -1844,7 +1846,7 @@ static void initRouterTable (Router *router) { #if defined(__APPLE__) os_log_with_type(SSC_OS_LOG_BUNDLE, OS_LOG_TYPE_INFO, "%{public}s", message.value.c_str()); #endif - stdWrite(message.value, false); + IO::write(message.value, false); }); /** @@ -1854,7 +1856,7 @@ static void initRouterTable (Router *router) { #if defined(__APPLE__) os_log_with_type(SSC_OS_LOG_BUNDLE, OS_LOG_TYPE_ERROR, "%{public}s", message.value.c_str()); #endif - stdWrite(message.value, true); + IO::write(message.value, true); }); /** diff --git a/src/ipc/ipc.cc b/src/ipc/ipc.cc index 7e96f23786..aba3eb68c4 100644 --- a/src/ipc/ipc.cc +++ b/src/ipc/ipc.cc @@ -86,7 +86,7 @@ namespace SSC::IPC { try { index = std::stoi(pair[1].size() > 0 ? pair[1] : "0"); } catch (...) { - std::cout << "Warning: received non-integer index" << std::endl; + debug("Warning: received non-integer index"); } } From 0aa6ac4eb9880940657e756e1063cb5830417558 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 22:27:25 -0400 Subject: [PATCH 177/256] refactor(extension): refactor to use single 'core.hh' --- src/extension/env.cc | 2 +- src/extension/extension.cc | 6 +++--- src/extension/extension.hh | 7 ++++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/extension/env.cc b/src/extension/env.cc index 59e858d045..5aa853c522 100644 --- a/src/extension/env.cc +++ b/src/extension/env.cc @@ -22,7 +22,7 @@ const char* sapi_env_get ( return nullptr; } - auto value = SSC::getEnv(name); + auto value = SSC::Env::get(name); if (value.size() == 0) { return nullptr; } diff --git a/src/extension/extension.cc b/src/extension/extension.cc index e72a6f238c..03df1c9abf 100644 --- a/src/extension/extension.cc +++ b/src/extension/extension.cc @@ -303,7 +303,7 @@ namespace SSC { // check if extension is already known if (isLoaded(name)) return true; - auto path = getExtensionsDirectory(name) + (name + SHARED_OBJ_EXT); + auto path = getExtensionsDirectory(name) + (name + RUNTIME_EXTENSION_FILE_EXT); #if defined(_WIN32) auto handle = LoadLibrary(path.c_str()); @@ -312,7 +312,7 @@ namespace SSC { if (!__sapi_extension_init) return false; #else #if defined(__ANDROID__) - auto handle = dlopen(String("libextension-" + name + SHARED_OBJ_EXT).c_str(), RTLD_NOW | RTLD_LOCAL); + auto handle = dlopen(String("libextension-" + name + RUNTIME_EXTENSION_FILE_EXT).c_str(), RTLD_NOW | RTLD_LOCAL); #else auto handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); #endif @@ -571,7 +571,7 @@ void sapi_log (const sapi_context_t* ctx, const char* message) { #if defined(__linux__) && defined(__ANDROID__) __android_log_print(ANDROID_LOG_INFO, "Console", "%s", message); #else - SSC::stdWrite(output, false); + SSC::IO::write(output, false); #endif #if defined(__APPLE__) diff --git a/src/extension/extension.hh b/src/extension/extension.hh index 1a0507e4ac..14336033b2 100644 --- a/src/extension/extension.hh +++ b/src/extension/extension.hh @@ -7,10 +7,15 @@ #include "../../include/socket/extension.h" #include "../process/process.hh" -#include "../core/json.hh" #include "../core/core.hh" #include "../ipc/ipc.hh" +#if defined(_WIN32) +#define RUNTIME_EXTENSION_FILE_EXT ".dll" +#else +#define RUNTIME_EXTENSION_FILE_EXT ".so" +#endif + namespace SSC { class Extension { public: From 1f41c8d3804eabbaf28fa597622210383b28cf3b Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 22:27:38 -0400 Subject: [PATCH 178/256] refactor(cli): refactor to use single 'core.hh' --- src/cli/cli.cc | 458 ++++++++++++++++++++++++------------------- src/cli/templates.hh | 15 ++ 2 files changed, 266 insertions(+), 207 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 3223434a0e..38a80bf68a 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -1,27 +1,6 @@ -#include "../common.hh" -#include "../core/core.hh" -#include "../core/file_system_watcher.hh" -#include "../process/process.hh" - -#include "templates.hh" - -#include <filesystem> -#include <unordered_set> -#include <algorithm> - -#ifdef __linux__ -#include <cstring> -#endif - -#if defined(__APPLE__) -#include <Foundation/Foundation.h> -#include <Cocoa/Cocoa.h> -#endif - -#include <sys/stat.h> -#include <sys/types.h> - -#ifdef _WIN32 +#if !defined(_WIN32) +#include <unistd.h> +#else #include <array> #include <AppxPackaging.h> #include <comdef.h> @@ -48,11 +27,25 @@ #pragma comment(lib, "libuv.lib") #pragma comment(lib, "winspool.lib") #pragma comment(lib, "ws2_32.lib") - -#else -#include <unistd.h> #endif +#include <sys/stat.h> +#include <sys/types.h> + +#include <algorithm> +#include <filesystem> +#include <fstream> +#include <iostream> +#include <regex> +#include <span> +#include <unordered_set> + +#include "../core/core.hh" +#include "../extension/extension.hh" +#include "../process/process.hh" + +#include "templates.hh" + #ifndef CMD_RUNNER #define CMD_RUNNER #endif @@ -64,6 +57,9 @@ using namespace SSC; using namespace std::chrono; +const auto DEFAULT_SSC_RC_FILENAME = String(".sscrc"); +const auto DEFAULT_SSC_ENV_FILENAME = String(".ssc.env"); + String _settings; Path targetPath; Map settings; @@ -88,18 +84,6 @@ static dispatch_queue_t queue = dispatch_queue_create( ); #endif -bool equal (const String& s1, const String& s2) { - return s1.compare(s2) == 0; -}; - -const Map SSC::getUserConfig () { - return settings; -} - -bool SSC::isDebugEnabled () { - return DEBUG == 1; -} - void log (const String s) { if (flagQuietMode) return; if (s.size() == 0) return; @@ -119,6 +103,53 @@ void log (const String s) { start = std::chrono::system_clock::now(); } +inline Map& extendMap (Map& dst, const Map& src) { + for (const auto& tuple : src) { + dst[tuple.first] = tuple.second; + } + return dst; +} + +inline String readFile (fs::path path) { + if (fs::is_directory(path)) { + log("WARNING: trying to read a directory as a file: " + path.string()); + return ""; + } + + std::ifstream stream(path.c_str()); + String content; + auto buffer = std::istreambuf_iterator<char>(stream); + auto end = std::istreambuf_iterator<char>(); + content.assign(buffer, end); + stream.close(); + return content; +} + +inline void writeFile (fs::path path, String s) { + std::ofstream stream(path.string()); + stream << s; + stream.close(); +} + +inline void appendFile (fs::path path, String s) { + std::ofstream stream; + stream.open(path.string(), std::ios_base::app); + stream << s; + stream.close(); +} + +bool equal (const String& s1, const String& s2) { + return s1.compare(s2) == 0; +}; + +const Map SSC::getUserConfig () { + return settings; +} + +bool SSC::isDebugEnabled () { + return DEBUG == 1; +} + void printHelp (const String& command) { if (command == "ssc") { std::cout << tmpl(gHelpText, defaultTemplateAttrs) << std::endl; @@ -140,13 +171,13 @@ void printHelp (const String& command) { } String getSocketHome (bool verbose) { - static String XDG_DATA_HOME = getEnv("XDG_DATA_HOME"); - static String LOCALAPPDATA = getEnv("LOCALAPPDATA"); - static String SOCKET_HOME = getEnv("SOCKET_HOME"); - static String HOME = getEnv("HOME"); + static String XDG_DATA_HOME = Env::get("XDG_DATA_HOME"); + static String LOCALAPPDATA = Env::get("LOCALAPPDATA"); + static String SOCKET_HOME = Env::get("SOCKET_HOME"); + static String HOME = Env::get("HOME"); static const bool SSC_DEBUG = ( - getEnv("SSC_DEBUG").size() > 0 || - getEnv("DEBUG").size() > 0 + Env::get("SSC_DEBUG").size() > 0 || + Env::get("DEBUG").size() > 0 ); static String socketHome = ""; @@ -176,7 +207,7 @@ String getSocketHome (bool verbose) { if (socketHome.size() > 0) { #ifdef _WIN32 - setEnv((String("SOCKET_HOME=") + socketHome).c_str()); + Env::set((String("SOCKET_HOME=") + socketHome).c_str()); #else setenv("SOCKET_HOME", socketHome.c_str(), 1); #endif @@ -194,7 +225,7 @@ String getSocketHome () { } String getAndroidHome () { - static auto androidHome = getEnv("ANDROID_HOME"); + static auto androidHome = Env::get("ANDROID_HOME"); if (androidHome.size() > 0) { return androidHome; } @@ -213,9 +244,9 @@ String getAndroidHome () { if (androidHome.size() == 0) { if (platform.mac) { - androidHome = getEnv("HOME") + "/Library/Android/sdk"; + androidHome = Env::get("HOME") + "/Library/Android/sdk"; } else if (platform.unix) { - androidHome = getEnv("HOME") + "/android"; + androidHome = Env::get("HOME") + "/android"; } else if (platform.win) { // TODO } @@ -223,14 +254,14 @@ String getAndroidHome () { if (androidHome.size() > 0) { #ifdef _WIN32 - setEnv((String("ANDROID_HOME=") + androidHome).c_str()); + Env::set((String("ANDROID_HOME=") + androidHome).c_str()); #else setenv("ANDROID_HOME", androidHome.c_str(), 1); #endif static const bool SSC_DEBUG = ( - getEnv("SSC_DEBUG").size() > 0 || - getEnv("DEBUG").size() > 0 + Env::get("SSC_DEBUG").size() > 0 || + Env::get("DEBUG").size() > 0 ); if (SSC_DEBUG) { @@ -462,11 +493,11 @@ void handleBuildPhaseForUserScript ( pathResourcesRelativeToUserBuild.string().size() ); - // @TODO(jwerle): use `setEnv()` if #148 is closed + // @TODO(jwerle): use `Env::set()` if #148 is closed #if _WIN32 String prefix_ = "PREFIX="; prefix_ += prefix; - setEnv(prefix_.c_str()); + Env::set(prefix_.c_str()); #else setenv("PREFIX", prefix, 1); #endif @@ -491,8 +522,8 @@ void handleBuildPhaseForUserScript ( buildScript, scriptArgs, (cwd / targetPath).string(), - [](SSC::String const &out) { stdWrite(out, false); }, - [](SSC::String const &out) { stdWrite(out, true); } + [](SSC::String const &out) { IO::write(out, false); }, + [](SSC::String const &out) { IO::write(out, true); } ); process->open(); @@ -523,8 +554,8 @@ void handleBuildPhaseForUserScript ( buildScript, scriptArgs, (cwd / targetPath).string(), - [](SSC::String const &out) { stdWrite(out, false); }, - [](SSC::String const &out) { stdWrite(out, true); }, + [](SSC::String const &out) { IO::write(out, false); }, + [](SSC::String const &out) { IO::write(out, true); }, [process](const auto status) { const auto exitCode = std::atoi(status.c_str()); if (exitCode != 0) { @@ -679,7 +710,7 @@ Vector<Path> handleBuildPhaseForCopyMappedFiles ( if (!fs::exists(fs::status(copyMapFile)) || !fs::is_regular_file(copyMapFile)) { log("WARNING: file specified in [build] copy_map does not exist"); } else { - auto copyMap = parseINI(tmpl(trim(readFile(copyMapFile)), settings)); + auto copyMap = INI::parse(tmpl(trim(readFile(copyMapFile)), settings)); auto copyMapFileDirectory = fs::absolute(copyMapFile.parent_path()); for (const auto& tuple : copyMap) { @@ -826,7 +857,7 @@ int runApp (const Path& path, const String& args, bool headless) { return 1; } - auto runner = trim(String(STR_VALUE(CMD_RUNNER))); + auto runner = trim(String(CONVERT_TO_STRING(CMD_RUNNER))); auto prefix = runner.size() > 0 ? runner + String(" ") : runner; String headlessCommand = ""; @@ -882,7 +913,7 @@ int runApp (const Path& path, const String& args, bool headless) { for (auto const &envKey : parseStringList(settings["build_env"])) { auto cleanKey = trim(envKey); - auto envValue = getEnv(cleanKey.c_str()); + auto envValue = Env::get(cleanKey.c_str()); auto key = [NSString stringWithUTF8String: cleanKey.c_str()]; auto value = [NSString stringWithUTF8String: envValue.c_str()]; @@ -1155,7 +1186,7 @@ void runIOSSimulator (const Path& path, Map& settings) { << " simctl launch --console --terminate-running-process booted" << " " + settings["meta_bundle_identifier"]; - setEnv("SIMCTL_CHILD_SSC_CLI_PID", std::to_string(getpid())); + Env::set("SIMCTL_CHILD_SSC_CLI_PID", std::to_string(getpid())); log("launching the app in simulator"); exit(std::system(launchAppCommand.str().c_str())); @@ -1228,9 +1259,9 @@ bool getAdbPath (AndroidCliState &state) { state.emulatorRunning = (deviceQuery.output.find("emulator") != SSC::String::npos); fs::current_path(cwd); - if (getEnv("SSC_ANDROID_TIMEOUT").size() > 0) { - state.androidTaskTimeout = std::stoi(getEnv("SSC_ANDROID_TIMEOUT")); - log("Using SSC_ANDROID_TIMEOUT=" + getEnv("SSC_ANDROID_TIMEOUT")); + if (Env::get("SSC_ANDROID_TIMEOUT").size() > 0) { + state.androidTaskTimeout = std::stoi(Env::get("SSC_ANDROID_TIMEOUT")); + log("Using SSC_ANDROID_TIMEOUT=" + Env::get("SSC_ANDROID_TIMEOUT")); } return true; @@ -1240,9 +1271,9 @@ bool setupAndroidAvd (AndroidCliState& state) { String package = state.quote + "system-images;" + state.platform + ";google_apis;" + replace(platform.arch, "arm64", "arm64-v8a") + state.quote; state.avdmanager << state.androidHome; - if (getEnv("ANDROID_SDK_MANAGER").size() > 0) + if (Env::get("ANDROID_SDK_MANAGER").size() > 0) { - state.avdmanager << "/" << replace(getEnv("ANDROID_SDK_MANAGER"), "sdkmanager", "avdmanager"); + state.avdmanager << "/" << replace(Env::get("ANDROID_SDK_MANAGER"), "sdkmanager", "avdmanager"); } else { if (!platform.win) { @@ -1499,7 +1530,7 @@ bool initAndStartAndroidApp (AndroidCliState& state, bool flagDebugMode) { } static String getCxxFlags () { - auto flags = getEnv("CXXFLAGS"); + auto flags = Env::get("CXXFLAGS"); return flags.size() > 0 ? " " + flags : ""; } @@ -1518,7 +1549,7 @@ inline String getCfgUtilPath () { } void initializeEnv (Path targetPath) { - static auto SSC_ENV_FILENAME = getEnv("SSC_ENV_FILENAME"); + static auto SSC_ENV_FILENAME = Env::get("SSC_ENV_FILENAME"); static auto filename = SSC_ENV_FILENAME.size() > 0 ? SSC_ENV_FILENAME : DEFAULT_SSC_ENV_FILENAME; @@ -1531,7 +1562,7 @@ void initializeEnv (Path targetPath) { } if (fs::exists(path) && fs::is_regular_file(path)) { - auto env = parseINI(readFile(path)); + auto env = INI::parse(readFile(path)); for (const auto& tuple : env) { auto key = tuple.first; auto value = tuple.second; @@ -1542,14 +1573,14 @@ void initializeEnv (Path targetPath) { value = valueAsPath.string(); } - setEnv(key, value); + Env::set(key, value); } } } void initializeRC (Path targetPath) { - static auto SSC_RC_FILENAME = getEnv("SSC_RC_FILENAME"); - static auto SSC_RC = getEnv("SSC_RC"); + static auto SSC_RC_FILENAME = Env::get("SSC_RC_FILENAME"); + static auto SSC_RC = Env::get("SSC_RC"); static auto filename = SSC_RC_FILENAME.size() > 0 ? SSC_RC_FILENAME : DEFAULT_SSC_RC_FILENAME; @@ -1564,7 +1595,7 @@ void initializeRC (Path targetPath) { } if (fs::exists(path) && fs::is_regular_file(path)) { - extendMap(rc, parseINI(readFile(path))); + extendMap(rc, INI::parse(readFile(path))); for (const auto& tuple : rc) { auto key = tuple.first; @@ -1579,7 +1610,7 @@ void initializeRC (Path targetPath) { // auto set environment variables if (key.starts_with("env_")) { key = key.substr(4, key.size() - 4); - setEnv(key, value); + Env::set(key, value); } } } @@ -1595,13 +1626,13 @@ bool isSetupCompleteAndroid () { return false; } - Path sdkManager = androidHome + "/" + getEnv("ANDROID_SDK_MANAGER"); + Path sdkManager = androidHome + "/" + Env::get("ANDROID_SDK_MANAGER"); if (!fs::exists(sdkManager)) { return false; } - if (!fs::exists(getEnv("JAVA_HOME"))) { + if (!fs::exists(Env::get("JAVA_HOME"))) { return false; } @@ -1609,11 +1640,11 @@ bool isSetupCompleteAndroid () { } bool isSetupCompleteWindows () { - if (getEnv("CXX").size() == 0) { + if (Env::get("CXX").size() == 0) { return false; } - return fs::exists(getEnv("CXX")); + return fs::exists(Env::get("CXX")); } bool isSetupComplete (SSC::String platform) { @@ -1746,12 +1777,12 @@ optionsAndEnv parseCommandLineOptions ( if (equal(key, "--verbose")) { flagVerboseMode = true; - setEnv("SSC_VERBOSE", "1"); + Env::set("SSC_VERBOSE", "1"); continue; } if (equal(key, "--debug")) { - setEnv("SSC_DEBUG", "1"); + Env::set("SSC_DEBUG", "1"); continue; } @@ -1869,9 +1900,9 @@ int main (const int argc, const char* argv[]) { const int numberOfOptions = argc - 2; #if defined(_WIN32) - static String HOME = getEnv("HOMEPATH"); + static String HOME = Env::get("HOMEPATH"); #else - static String HOME = getEnv("HOME"); + static String HOME = Env::get("HOME"); #endif // `/etc/sscrc` (global) @@ -1995,8 +2026,8 @@ int main (const int argc, const char* argv[]) { auto parts = split(value, '='); if (parts.size() == 2) { stream << parts[0] << " = " << parts[1] << "\n"; - } else if (parts.size() == 1 && hasEnv(parts[0])) { - stream << parts[0] << " = " << getEnv(parts[0]) << "\n"; + } else if (parts.size() == 1 && Env::has(parts[0])) { + stream << parts[0] << " = " << Env::get(parts[0]) << "\n"; } } @@ -2021,7 +2052,7 @@ int main (const int argc, const char* argv[]) { ini += "\n"; if (configExists) { - auto hex = stringToHex(ini); + auto hex = encodeHexString(ini); auto bytes = StringStream(); auto length = hex.size() - 1; int size = 0; @@ -2048,7 +2079,7 @@ int main (const int argc, const char* argv[]) { ); } - settings = parseINI(ini); + settings = INI::parse(ini); if (settings["meta_type"] == "extension" || settings["build_type"] == "extension") { auto extension = settings["build_name"]; @@ -2163,13 +2194,13 @@ int main (const int argc, const char* argv[]) { if (projectName.size() > 0) { defaultTemplateAttrs["project_name"] = projectName; } - SSC::writeFile(targetPath / "socket.ini", tmpl(gDefaultConfig, defaultTemplateAttrs)); + writeFile(targetPath / "socket.ini", tmpl(gDefaultConfig, defaultTemplateAttrs)); log("socket.ini created in " + targetPath.string()); } if (!configOnly) { if (isCurrentPathEmpty) { fs::create_directories(targetPath / "src"); - SSC::writeFile(targetPath / "src" / "index.html", gHelloWorld); + writeFile(targetPath / "src" / "index.html", gHelloWorld); log("src/index.html created in " + targetPath.string()); } else { log("Current directory was not empty. Assuming index.html is already in place."); @@ -2177,7 +2208,7 @@ int main (const int argc, const char* argv[]) { if (fs::exists(targetPath / ".gitignore")) { log(".gitignore already exists in " + targetPath.string()); } else { - SSC::writeFile(targetPath / ".gitignore", gDefaultGitignore); + writeFile(targetPath / ".gitignore", gDefaultGitignore); log(".gitignore created in " + targetPath.string()); } } @@ -2667,14 +2698,14 @@ int main (const int argc, const char* argv[]) { androidState.targetPlatform = targetPlatform; const bool debugEnv = ( - getEnv("SSC_DEBUG").size() > 0 || - getEnv("DEBUG").size() > 0 + Env::get("SSC_DEBUG").size() > 0 || + Env::get("DEBUG").size() > 0 ); auto debugBuild = debugEnv; const bool verboseEnv = ( - getEnv("SSC_VERBOSE").size() > 0 || - getEnv("VERBOSE").size() > 0 + Env::get("SSC_VERBOSE").size() > 0 || + Env::get("VERBOSE").size() > 0 ); auto oldCwd = fs::current_path(); @@ -2691,17 +2722,17 @@ int main (const int argc, const char* argv[]) { // set it automatically, hide it from the user settings["meta_revision"] = "1"; - if (getEnv("CXX").size() == 0) { + if (Env::get("CXX").size() == 0) { log("WARNING: $CXX env var not set, assuming defaults"); if (platform.win) { - setEnv("CXX=clang++"); + Env::set("CXX=clang++"); } else { - setEnv("CXX=/usr/bin/clang++"); + Env::set("CXX=/usr/bin/clang++"); } } - if (getEnv("CI").size() == 0 && !isSetupComplete(platformFriendlyName)) { + if (Env::get("CI").size() == 0 && !isSetupComplete(platformFriendlyName)) { log("Build dependency setup is incomplete for " + platformFriendlyName + ", Use 'setup' to resolve: "); std::cout << "ssc setup --platform=" << platformFriendlyName << std::endl; exit(1); @@ -2717,7 +2748,7 @@ int main (const int argc, const char* argv[]) { flagRunUserBuildOnly = false; } else { struct stat stats; - if (stat(WStringToString(binaryPath).c_str(), &stats) == 0) { + if (stat(convertWStringToString(binaryPath).c_str(), &stats) == 0) { if (SSC_BUILD_TIME > stats.st_mtime) { flagRunUserBuildOnly = false; } @@ -2728,8 +2759,8 @@ int main (const int argc, const char* argv[]) { struct stat binaryPathStats; struct stat configPathStats; - if (stat(WStringToString(binaryPath).c_str(), &binaryPathStats) == 0) { - if (stat(WStringToString(configPath).c_str(), &configPathStats) == 0) { + if (stat(convertWStringToString(binaryPath).c_str(), &binaryPathStats) == 0) { + if (stat(convertWStringToString(configPath).c_str(), &configPathStats) == 0) { if (configPathStats.st_mtime > binaryPathStats.st_mtime) { flagRunUserBuildOnly = false; } @@ -2962,9 +2993,9 @@ int main (const int argc, const char* argv[]) { if (debugEnv || verboseEnv) log("sdkmanager --version 2>&1 >/dev/null"); sdkmanager << androidHome; - if (getEnv("ANDROID_SDK_MANAGER").size() > 0) + if (Env::get("ANDROID_SDK_MANAGER").size() > 0) { - sdkmanager << "/" << getEnv("ANDROID_SDK_MANAGER"); + sdkmanager << "/" << Env::get("ANDROID_SDK_MANAGER"); } else if (std::system(" sdkmanager --version 2>&1 >/dev/null") != 0) { if (!platform.win) { sdkmanager << "/cmdline-tools/latest/bin/sdkmanager"; @@ -2984,7 +3015,7 @@ int main (const int argc, const char* argv[]) { auto cmdQuote = platform.win ? "\"" : ""; // redirect yes stderr to stdout, this hides "broken pipe" / "no space left on device" errors that are caused by sdkmanager terminating normally String licenseAccept = - cmdQuote + quote + (getEnv("ANDROID_SDK_MANAGER_ACCEPT_LICENSES").size() > 0 ? (getEnv("ANDROID_SDK_MANAGER_ACCEPT_LICENSES")) : "echo") + quote + + cmdQuote + quote + (Env::get("ANDROID_SDK_MANAGER_ACCEPT_LICENSES").size() > 0 ? (Env::get("ANDROID_SDK_MANAGER_ACCEPT_LICENSES")) : "echo") + quote + (platform.win ? " 2>&1" : "") + " | " + quote + sdkmanager.str() + quote + " --licenses" + cmdQuote; @@ -2997,7 +3028,7 @@ int main (const int argc, const char* argv[]) { // TODO: internal, no need to save to settings settings["android_sdk_manager_path"] = sdkmanager.str(); - String gradlePath = getEnv("GRADLE_HOME").size() > 0 ? getEnv("GRADLE_HOME") + slash + "bin" + slash : ""; + String gradlePath = Env::get("GRADLE_HOME").size() > 0 ? Env::get("GRADLE_HOME") + slash + "bin" + slash : ""; StringStream gradleInitCommand; gradleInitCommand @@ -3018,7 +3049,7 @@ int main (const int argc, const char* argv[]) { log( String("Check licenses and run again: \n") + (platform.win ? "set" : "export") + - (" JAVA_HOME=\"" + getEnv("JAVA_HOME") + "\" && ") + + (" JAVA_HOME=\"" + Env::get("JAVA_HOME") + "\" && ") + licenseAccept + "\n" ); @@ -3026,16 +3057,21 @@ int main (const int argc, const char* argv[]) { } // Core - fs::copy(trim(prefixFile("src/common.hh")), jni, fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/config.hh")), jni, fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/init.cc")), jni, fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/android/bridge.cc")), jni / "android", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/android/runtime.cc")), jni / "android", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/android/string_wrap.cc")), jni / "android", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/android/window.cc")), jni / "android", fs::copy_options::overwrite_existing); + fs::copy(trim(prefixFile("src/core/config.hh")), jni, fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/core/core.hh")), jni / "core", fs::copy_options::overwrite_existing); + fs::copy(trim(prefixFile("src/core/debug.hh")), jni / "core", fs::copy_options::overwrite_existing); + fs::copy(trim(prefixFile("src/core/env.hh")), jni / "core", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/core/json.hh")), jni / "core", fs::copy_options::overwrite_existing); + fs::copy(trim(prefixFile("src/core/platform.hh")), jni / "core", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/core/preload.hh")), jni / "core", fs::copy_options::overwrite_existing); + fs::copy(trim(prefixFile("src/core/string.hh")), jni / "core", fs::copy_options::overwrite_existing); + fs::copy(trim(prefixFile("src/core/types.hh")), jni / "core", fs::copy_options::overwrite_existing); + fs::copy(trim(prefixFile("src/core/version.hh")), jni / "core", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/ipc/ipc.hh")), jni / "ipc", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/window/options.hh")), jni / "window", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/window/window.hh")), jni / "window", fs::copy_options::overwrite_existing); @@ -3100,7 +3136,7 @@ int main (const int argc, const char* argv[]) { } if (settings["android_native_abis"].size() == 0) { - settings["android_native_abis"] = getEnv("ANDROID_SUPPORTED_ABIS"); + settings["android_native_abis"] = Env::get("ANDROID_SUPPORTED_ABIS"); } if (settings["android_ndk_abi_filters"].size() == 0) { @@ -3278,7 +3314,7 @@ int main (const int argc, const char* argv[]) { writeFile( jni / "src" / filename, tmpl(std::regex_replace( - WStringToString(readFile(targetPath / file )), + convertWStringToString(readFile(targetPath / file )), std::regex("__BUNDLE_IDENTIFIER__"), bundle_identifier ), settings) @@ -3337,7 +3373,7 @@ int main (const int argc, const char* argv[]) { if (fs::exists(target)) { auto configFile = target / "socket.ini"; - auto config = parseINI(fs::exists(configFile) ? readFile(configFile) : ""); + auto config = INI::parse(fs::exists(configFile) ? readFile(configFile) : ""); settings["build_extensions_" + extension + "_path"] = target.string(); for (const auto& entry : config) { @@ -3467,7 +3503,7 @@ int main (const int argc, const char* argv[]) { Path(source).filename().string() ).string(); - if (getEnv("DEBUG") == "1" || getEnv("VERBOSE") == "1") { + if (Env::get("DEBUG") == "1" || Env::get("VERBOSE") == "1") { log("extension source: " + source); } @@ -3516,7 +3552,7 @@ int main (const int argc, const char* argv[]) { } } - make << "## socket/extensions/" << extension << SHARED_OBJ_EXT << std::endl; + make << "## socket/extensions/" << extension << RUNTIME_EXTENSION_FILE_EXT << std::endl; make << "include $(CLEAR_VARS)" << std::endl; make << "LOCAL_MODULE := extension-" << extension << std::endl; make << std::endl; @@ -3606,7 +3642,7 @@ int main (const int argc, const char* argv[]) { if (settings["android_native_makefile"].size() > 0) { makefileContext["android_native_make_context"] = - trim(tmpl(tmpl(WStringToString(readFile(targetPath / settings["android_native_makefile"])), settings), makefileContext)); + trim(tmpl(tmpl(convertWStringToString(readFile(targetPath / settings["android_native_makefile"])), settings), makefileContext)); } else { makefileContext["android_native_make_context"] = ""; } @@ -3625,7 +3661,7 @@ int main (const int argc, const char* argv[]) { writeFile( jni / "android" / "internal.hh", std::regex_replace( - WStringToString(readFile(trim(prefixFile("src/android/internal.hh")))), + convertWStringToString(readFile(trim(prefixFile("src/android/internal.hh")))), std::regex("__BUNDLE_IDENTIFIER__"), bundle_path_underscored ) @@ -3634,7 +3670,7 @@ int main (const int argc, const char* argv[]) { writeFile( pkg / "bridge.kt", std::regex_replace( - WStringToString(readFile(trim(prefixFile("src/android/bridge.kt")))), + convertWStringToString(readFile(trim(prefixFile("src/android/bridge.kt")))), std::regex("__BUNDLE_IDENTIFIER__"), bundle_identifier ) @@ -3643,7 +3679,7 @@ int main (const int argc, const char* argv[]) { writeFile( pkg / "main.kt", std::regex_replace( - WStringToString(readFile(trim(prefixFile("src/android/main.kt")))), + convertWStringToString(readFile(trim(prefixFile("src/android/main.kt")))), std::regex("__BUNDLE_IDENTIFIER__"), bundle_identifier ) @@ -3652,7 +3688,7 @@ int main (const int argc, const char* argv[]) { writeFile( pkg / "runtime.kt", std::regex_replace( - WStringToString(readFile(trim(prefixFile("src/android/runtime.kt")))), + convertWStringToString(readFile(trim(prefixFile("src/android/runtime.kt")))), std::regex("__BUNDLE_IDENTIFIER__"), bundle_identifier ) @@ -3661,7 +3697,7 @@ int main (const int argc, const char* argv[]) { writeFile( pkg / "window.kt", std::regex_replace( - WStringToString(readFile(trim(prefixFile("src/android/window.kt")))), + convertWStringToString(readFile(trim(prefixFile("src/android/window.kt")))), std::regex("__BUNDLE_IDENTIFIER__"), bundle_identifier ) @@ -3670,7 +3706,7 @@ int main (const int argc, const char* argv[]) { writeFile( pkg / "webview.kt", std::regex_replace( - WStringToString(readFile(trim(prefixFile("src/android/webview.kt")))), + convertWStringToString(readFile(trim(prefixFile("src/android/webview.kt")))), std::regex("__BUNDLE_IDENTIFIER__"), bundle_identifier ) @@ -3682,7 +3718,7 @@ int main (const int argc, const char* argv[]) { writeFile( pkg / Path(file).filename(), tmpl(std::regex_replace( - WStringToString(readFile(targetPath / file )), + convertWStringToString(readFile(targetPath / file )), std::regex("__BUNDLE_IDENTIFIER__"), bundle_identifier ), settings) @@ -3747,7 +3783,7 @@ int main (const int argc, const char* argv[]) { String team = matchTeamId.str(1); - auto pathToInstalledProfile = Path(getEnv("HOME")) / + auto pathToInstalledProfile = Path(Env::get("HOME")) / "Library" / "MobileDevice" / "Provisioning Profiles" / @@ -3880,7 +3916,7 @@ int main (const int argc, const char* argv[]) { if (fs::exists(target)) { auto configFile = target / "socket.ini"; - auto config = parseINI(fs::exists(configFile) ? readFile(configFile) : ""); + auto config = INI::parse(fs::exists(configFile) ? readFile(configFile) : ""); settings["build_extensions_" + extension + "_path"] = target.string(); for (const auto& entry : config) { @@ -3958,7 +3994,7 @@ int main (const int argc, const char* argv[]) { } for (auto source : sources) { - if (getEnv("DEBUG") == "1" || getEnv("VERBOSE") == "1") { + if (Env::get("DEBUG") == "1" || Env::get("VERBOSE") == "1") { log("extension source: " + source); } @@ -4042,12 +4078,12 @@ int main (const int argc, const char* argv[]) { << " -fembed-bitcode" << " -fPIC" << " " << trim(compilerFlags + " " + (flagDebugMode ? compilerDebugFlags : "")) - << " " << (flagBuildForSimulator ? "-mios-simulator-version-min=" : "-miphoneos-version-min=") + getEnv("IPHONEOS_VERSION_MIN", "15.0") + << " " << (flagBuildForSimulator ? "-mios-simulator-version-min=" : "-miphoneos-version-min=") + Env::get("IPHONEOS_VERSION_MIN", "15.0") << " -c " << source << " -o " << object.string() ; - if (getEnv("DEBUG") == "1" || getEnv("VERBOSE") == "1") { + if (Env::get("DEBUG") == "1" || Env::get("VERBOSE") == "1") { log(compileExtensionObjectCommand.str()); } @@ -4081,7 +4117,7 @@ int main (const int argc, const char* argv[]) { linkerFlags += " -arch arm64 "; } - auto lib = (paths.pathResourcesRelativeToUserBuild / "socket" / "extensions" / extension / (extension + SHARED_OBJ_EXT)); + auto lib = (paths.pathResourcesRelativeToUserBuild / "socket" / "extensions" / extension / (extension + RUNTIME_EXTENSION_FILE_EXT)); fs::create_directories(lib.parent_path()); auto compileExtensionLibraryCommand = StringStream(); compileExtensionLibraryCommand @@ -4113,12 +4149,12 @@ int main (const int argc, const char* argv[]) { << " -shared" << " -v" << " -target " << (flagBuildForSimulator ? platform.arch + "-apple-ios-simulator": "arm64-apple-ios") - << " " << (flagBuildForSimulator ? "-mios-simulator-version-min=" : "-miphoneos-version-min=") + getEnv("IPHONEOS_VERSION_MIN", "15.0") + << " " << (flagBuildForSimulator ? "-mios-simulator-version-min=" : "-miphoneos-version-min=") + Env::get("IPHONEOS_VERSION_MIN", "15.0") << " " << trim(linkerFlags + " " + (flagDebugMode ? linkerDebugFlags : "")) << " -o " << lib ; - if (getEnv("DEBUG") == "1" || getEnv("VERBOSE") == "1") { + if (Env::get("DEBUG") == "1" || Env::get("VERBOSE") == "1") { log(compileExtensionLibraryCommand.str()); } @@ -4290,7 +4326,7 @@ int main (const int argc, const char* argv[]) { auto missing_assets = false; auto debugBuild = debugEnv; if (debugBuild) { - for (String libString : split(getEnv("WIN_DEBUG_LIBS"), ',')) { + for (String libString : split(Env::get("WIN_DEBUG_LIBS"), ',')) { if (libString.size() > 0) { if (libString[0] == '\"' && libString[libString.size()-2] == '\"') libString = libString.substr(1, libString.size()-2); @@ -4379,7 +4415,7 @@ int main (const int argc, const char* argv[]) { log("package prepared"); - auto SOCKET_HOME_API = getEnv("SOCKET_HOME_API"); + auto SOCKET_HOME_API = Env::get("SOCKET_HOME_API"); if (SOCKET_HOME_API.size() == 0) { SOCKET_HOME_API = trim(prefixFile("api")); @@ -4404,18 +4440,26 @@ int main (const int argc, const char* argv[]) { exit(1); } - log("building for iOS"); + if (flagBuildForSimulator) { + log("building for iOS Simulator"); + } else { + log("building for iOS"); + } auto pathToDist = oldCwd / paths.platformSpecificOutputPath; fs::create_directories(pathToDist); + fs::create_directories(pathToDist / "core"); fs::current_path(pathToDist); // // Copy and or create the source files we need for the build. // - fs::copy(trim(prefixFile("src/common.hh")), pathToDist); fs::copy(trim(prefixFile("src/init.cc")), pathToDist); + fs::copy(trim(prefixFile("src/core/config.hh")), pathToDist / "core"); + fs::copy(trim(prefixFile("src/core/string.hh")), pathToDist / "core"); + fs::copy(trim(prefixFile("src/core/types.hh")), pathToDist / "core"); + fs::copy(trim(prefixFile("src/core/ini.hh")), pathToDist / "core"); auto pathBase = pathToDist / "Base.lproj"; fs::create_directories(pathBase); @@ -4610,13 +4654,13 @@ int main (const int argc, const char* argv[]) { sdkmanager << packages.str(); - if (getEnv("SSC_SKIP_ANDROID_SDK_MANAGER").size() == 0) { + if (Env::get("SSC_SKIP_ANDROID_SDK_MANAGER").size() == 0) { if (debugEnv || verboseEnv) { log(sdkmanager.str()); } if (std::system(sdkmanager.str().c_str()) != 0) { - log("WARN: failed to initialize Android SDK (sdkmanager)"); + log("WARNING: failed to initialize Android SDK (sdkmanager)"); } } @@ -4704,7 +4748,7 @@ int main (const int argc, const char* argv[]) { // libsocket-runtime.so count doesn't match the static lib count - There should be one libuv.a and libsocket-runtime.a for each android architecture if (androidSharedLibCount > 0 && androidSharedLibCount != (androidStaticLibCount / 2)) { buildJniLibs = true; - log("WARN: Android Shared Lib Count is incorrect, forcing rebuild."); + log("WARNING: Android Shared Lib Count is incorrect, forcing rebuild."); fs::remove_all(jniLibs); } else { buildJniLibs = false; @@ -4737,7 +4781,7 @@ int main (const int argc, const char* argv[]) { } // just build for CI - if (getEnv("SSC_CI").size() > 0) { + if (Env::get("SSC_CI").size() > 0) { StringStream gradlew; gradlew << localDirPrefix << "gradlew build"; @@ -4751,7 +4795,7 @@ int main (const int argc, const char* argv[]) { // Check for gradle in pwd. Don't fail, this is just for support. if (!fs::exists(String("gradlew") + (platform.win ? ".bat" : ""))) { - log("WARN: gradlew script not in pwd: " + fs::current_path().string()); + log("WARNING: gradlew script not in pwd: " + fs::current_path().string()); } String bundle = flagDebugMode ? @@ -4791,13 +4835,13 @@ int main (const int argc, const char* argv[]) { : settings.count("build_flags") ? settings["build_flags"] : ""; quote = ""; - if (platform.win && getEnv("CXX").find(" ") != SSC::String::npos) { + if (platform.win && Env::get("CXX").find(" ") != SSC::String::npos) { quote = "\""; } // build desktop extension if (isForDesktop) { - static const auto IN_GITHUB_ACTIONS_CI = getEnv("GITHUB_ACTIONS_CI").size() == 0; + static const auto IN_GITHUB_ACTIONS_CI = Env::get("GITHUB_ACTIONS_CI").size() == 0; auto oldCwd = fs::current_path(); fs::current_path(targetPath); @@ -4866,7 +4910,7 @@ int main (const int argc, const char* argv[]) { target = fs::canonical(target); auto configFile = target / "socket.ini"; - auto config = parseINI(fs::exists(configFile) ? readFile(configFile) : ""); + auto config = INI::parse(fs::exists(configFile) ? readFile(configFile) : ""); settings["build_extensions_" + extension + "_path"] = target.string(); fs::current_path(target); @@ -4888,7 +4932,7 @@ int main (const int argc, const char* argv[]) { auto match = std::smatch{}; while (std::regex_search(value, match, std::regex("\\$\\((.*?)\\)"))) { auto subcommand = match[1].str(); - if (getEnv("DEBUG") == "1" || getEnv("VERBOSE") == "1") { + if (Env::get("DEBUG") == "1" || Env::get("VERBOSE") == "1") { log("Running subcommand: " + subcommand); } auto proc = exec(subcommand); @@ -4906,7 +4950,7 @@ int main (const int argc, const char* argv[]) { auto envVar = match[1].str(); auto envValue = envVar == "PWD" ? target.string() // Use the extension root. - : getEnv(envVar); + : Env::get(envVar); if (envValue.size() == 0) { log("ERROR: failed to find env var: " + envVar); exit(1); @@ -4964,7 +5008,7 @@ int main (const int argc, const char* argv[]) { ); auto objects = StringStream(); - auto lib = (paths.pathResourcesRelativeToUserBuild / "socket" / "extensions" / extension / (extension + SHARED_OBJ_EXT)); + auto lib = (paths.pathResourcesRelativeToUserBuild / "socket" / "extensions" / extension / (extension + RUNTIME_EXTENSION_FILE_EXT)); fs::create_directories(lib.parent_path()); @@ -5025,7 +5069,7 @@ int main (const int argc, const char* argv[]) { ); for (auto source : sources) { - if (getEnv("DEBUG") == "1" || getEnv("VERBOSE") == "1") { + if (Env::get("DEBUG") == "1" || Env::get("VERBOSE") == "1") { log("extension source: " + source); } @@ -5058,8 +5102,8 @@ int main (const int argc, const char* argv[]) { compilerDebugFlags += "-D_DEBUG"; } - auto CXX = getEnv("CXX"); - auto CC = getEnv("CC"); + auto CXX = Env::get("CXX"); + auto CC = Env::get("CC"); String compiler; if (source.ends_with(".hh") || source.ends_with(".h")) { @@ -5099,7 +5143,7 @@ int main (const int argc, const char* argv[]) { continue; } - if (getEnv("DEBUG") == "1" || getEnv("VERBOSE") == "1") { + if (Env::get("DEBUG") == "1" || Env::get("VERBOSE") == "1") { log("extension source: " + source); } @@ -5156,16 +5200,16 @@ int main (const int argc, const char* argv[]) { struct stat libraryStats; if (fs::exists(object)) { - if (stat(WStringToString(source).c_str(), &sourceStats) == 0) { - if (stat(WStringToString(object).c_str(), &objectStats) == 0) { + if (stat(convertWStringToString(source).c_str(), &sourceStats) == 0) { + if (stat(convertWStringToString(object).c_str(), &objectStats) == 0) { if (objectStats.st_mtime > sourceStats.st_mtime) { continue; } } } - if (stat(WStringToString(source).c_str(), &sourceStats) == 0) { - if (stat(WStringToString(lib).c_str(), &libraryStats) == 0) { + if (stat(convertWStringToString(source).c_str(), &sourceStats) == 0) { + if (stat(convertWStringToString(lib).c_str(), &libraryStats) == 0) { if (libraryStats.st_mtime > sourceStats.st_mtime) { continue; } @@ -5173,7 +5217,7 @@ int main (const int argc, const char* argv[]) { } } - if (getEnv("DEBUG") == "1" || getEnv("VERBOSE") == "1") { + if (Env::get("DEBUG") == "1" || Env::get("VERBOSE") == "1") { log(compileExtensionObjectCommand.str()); } @@ -5204,7 +5248,7 @@ int main (const int argc, const char* argv[]) { if (platform.win && debugBuild) { linkerDebugFlags += "-D_DEBUG"; - for (String libString : split(getEnv("WIN_DEBUG_LIBS"), ',')) { + for (String libString : split(Env::get("WIN_DEBUG_LIBS"), ',')) { if (libString.size() > 0) { if (libString[0] == '\"' && libString[libString.size()-2] == '\"') { libString = libString.substr(1, libString.size()-2); @@ -5235,7 +5279,7 @@ int main (const int argc, const char* argv[]) { compileExtensionLibraryCommand << quote // win32 - quote the entire command << quote // win32 - quote the binary path - << getEnv("CXX") + << Env::get("CXX") << quote // win32 - quote the binary path << " " << static_runtime << " " << static_uv @@ -5274,11 +5318,11 @@ int main (const int argc, const char* argv[]) { if (platform.mac) { if (isForDesktop) { - settings["mac_codesign_paths"] += (paths.pathResourcesRelativeToUserBuild / "socket" / "extensions" / extension / (extension + SHARED_OBJ_EXT)).string() + ";"; + settings["mac_codesign_paths"] += (paths.pathResourcesRelativeToUserBuild / "socket" / "extensions" / extension / (extension + RUNTIME_EXTENSION_FILE_EXT)).string() + ";"; } } - if (getEnv("DEBUG") == "1" || getEnv("VERBOSE") == "1") { + if (Env::get("DEBUG") == "1" || Env::get("VERBOSE") == "1") { log(compileExtensionLibraryCommand.str()); } @@ -5311,7 +5355,7 @@ int main (const int argc, const char* argv[]) { compileCommand << quote // win32 - quote the entire command << quote // win32 - quote the binary path - << getEnv("CXX") + << Env::get("CXX") << quote // win32 - quote the binary path << " " << files << " " << flags @@ -5328,7 +5372,7 @@ int main (const int argc, const char* argv[]) { << quote // win32 - quote the entire command ; - if (getEnv("DEBUG") == "1" || getEnv("VERBOSE") == "1") + if (Env::get("DEBUG") == "1" || Env::get("VERBOSE") == "1") log(compileCommand.str()); auto r = exec(compileCommand.str()); @@ -5703,8 +5747,8 @@ int main (const int argc, const char* argv[]) { // if (flagShouldNotarize && platform.mac && isForDesktop) { StringStream notarizeCommand; - String username = getEnv("APPLE_ID"); - String password = getEnv("APPLE_ID_PASSWORD"); + String username = Env::get("APPLE_ID"); + String password = Env::get("APPLE_ID_PASSWORD"); if (username.size() == 0) { username = settings["apple_identifier"]; @@ -5923,7 +5967,7 @@ int main (const int argc, const char* argv[]) { return hr; }; - WString appx(SSC::StringToWString(paths.pathPackage.string()) + L".appx"); + WString appx(SSC::convertStringToWString(paths.pathPackage.string()) + L".appx"); HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); @@ -5974,7 +6018,7 @@ int main (const int argc, const char* argv[]) { if (SUCCEEDED(hr2)) { } else { - log("mimetype?: " + WStringToString(mime)); + log("mimetype?: " + convertWStringToString(mime)); log("Could not add payload file: " + entry.path().string()); } } else { @@ -6033,7 +6077,7 @@ int main (const int argc, const char* argv[]) { } else { _com_error err(hr); - String msg = WStringToString( err.ErrorMessage() ); + String msg = convertWStringToString( err.ErrorMessage() ); log("Unable to save package; " + msg); exit(1); @@ -6051,7 +6095,7 @@ int main (const int argc, const char* argv[]) { // https://www.digicert.com/kb/code-signing/signcode-signtool-command-line.htm // auto sdkRoot = Path("C:\\Program Files (x86)\\Windows Kits\\10\\bin"); - auto pathToSignTool = getEnv("SIGNTOOL"); + auto pathToSignTool = Env::get("SIGNTOOL"); if (pathToSignTool.size() == 0) { // TODO assumes the last dir that contains dot. posix doesnt guarantee @@ -6074,10 +6118,10 @@ int main (const int argc, const char* argv[]) { } StringStream signCommand; - String password = getEnv("CSC_KEY_PASSWORD"); + String password = Env::get("CSC_KEY_PASSWORD"); if (password.size() == 0) { - log("ERROR: Environment variable 'CSC_KEY_PASSWORD' is empty!"); + log("ERROR: Env variable 'CSC_KEY_PASSWORD' is empty!"); exit(1); } @@ -6129,7 +6173,7 @@ int main (const int argc, const char* argv[]) { auto settingsForSourcesWatcher = settings; extendMap( settingsForSourcesWatcher, - parseINI(readFile(targetPath / "socket.ini")) + INI::parse(readFile(targetPath / "socket.ini")) ); handleBuildPhaseForUserScript( @@ -6225,13 +6269,13 @@ int main (const int argc, const char* argv[]) { settings.insert(std::make_pair("port", devPort)); const bool debugEnv = ( - getEnv("SSC_DEBUG").size() > 0 || - getEnv("DEBUG").size() > 0 + Env::get("SSC_DEBUG").size() > 0 || + Env::get("DEBUG").size() > 0 ); const bool verboseEnv = ( - getEnv("SSC_VERBOSE").size() > 0 || - getEnv("VERBOSE").size() > 0 + Env::get("SSC_VERBOSE").size() > 0 || + Env::get("VERBOSE").size() > 0 ); Paths paths = getPaths(targetPlatform); @@ -6361,54 +6405,54 @@ int main (const int argc, const char* argv[]) { createSubcommand("env", {}, true, [&](Map optionsWithValue, std::unordered_set<String> optionsWithoutValue) -> void { auto envs = Map(); - envs["DEBUG"] = getEnv("DEBUG"); - envs["VERBOSE"] = getEnv("VERBOSE"); + envs["DEBUG"] = Env::get("DEBUG"); + envs["VERBOSE"] = Env::get("VERBOSE"); // runtime variables envs["SOCKET_HOME"] = getSocketHome(false); - envs["SOCKET_HOME_API"] = getEnv("SOCKET_HOME_API"); + envs["SOCKET_HOME_API"] = Env::get("SOCKET_HOME_API"); // platform OS variables - envs["PWD"] = getEnv("PWD"); - envs["HOME"] = getEnv("HOME"); - envs["LANG"] = getEnv("LANG"); - envs["USER"] = getEnv("USER"); - envs["SHELL"] = getEnv("SHELL"); - envs["HOMEPATH"] = getEnv("HOMEPATH"); - envs["LOCALAPPDATA"] = getEnv("LOCALAPPDATA"); - envs["XDG_DATA_HOME"] = getEnv("XDG_DATA_HOME"); + envs["PWD"] = Env::get("PWD"); + envs["HOME"] = Env::get("HOME"); + envs["LANG"] = Env::get("LANG"); + envs["USER"] = Env::get("USER"); + envs["SHELL"] = Env::get("SHELL"); + envs["HOMEPATH"] = Env::get("HOMEPATH"); + envs["LOCALAPPDATA"] = Env::get("LOCALAPPDATA"); + envs["XDG_DATA_HOME"] = Env::get("XDG_DATA_HOME"); // compiler variables - envs["CXX"] = getEnv("CXX"); - envs["PREFIX"] = getEnv("PREFIX"); - envs["CXXFLAGS"] = getEnv("CXXFLAGS"); + envs["CXX"] = Env::get("CXX"); + envs["PREFIX"] = Env::get("PREFIX"); + envs["CXXFLAGS"] = Env::get("CXXFLAGS"); // locale variables - envs["LC_ALL"] = getEnv("LC_ALL"); - envs["LC_CTYPE"] = getEnv("LC_CTYPE"); - envs["LC_TERMINAL"] = getEnv("LC_TERMINAL"); - envs["LC_TERMINAL_VERSION"] = getEnv("LC_TERMINAL_VERSION"); + envs["LC_ALL"] = Env::get("LC_ALL"); + envs["LC_CTYPE"] = Env::get("LC_CTYPE"); + envs["LC_TERMINAL"] = Env::get("LC_TERMINAL"); + envs["LC_TERMINAL_VERSION"] = Env::get("LC_TERMINAL_VERSION"); // platform dependency variables - envs["JAVA_HOME"] = getEnv("JAVA_HOME"); - envs["GRADLE_HOME"] = getEnv("GRADLE_HOME"); + envs["JAVA_HOME"] = Env::get("JAVA_HOME"); + envs["GRADLE_HOME"] = Env::get("GRADLE_HOME"); envs["ANDROID_HOME"] = getAndroidHome(); - envs["ANDROID_SUPPORTED_ABIS"] = getEnv("ANDROID_SUPPORTED_ABIS"); + envs["ANDROID_SUPPORTED_ABIS"] = Env::get("ANDROID_SUPPORTED_ABIS"); // apple specific platform variables - envs["APPLE_ID"] = getEnv("APPLE_ID"); - envs["APPLE_ID_PASSWORD"] = getEnv("APPLE_ID"); + envs["APPLE_ID"] = Env::get("APPLE_ID"); + envs["APPLE_ID_PASSWORD"] = Env::get("APPLE_ID"); // windows specific platform variables - envs["SIGNTOOL"] = getEnv("SIGNTOOL"); - envs["WIN_DEBUG_LIBS"] = getEnv("WIN_DEBUG_LIBS"); - envs["CSC_KEY_PASSWORD"] = getEnv("CSC_KEY_PASSWORD"); + envs["SIGNTOOL"] = Env::get("SIGNTOOL"); + envs["WIN_DEBUG_LIBS"] = Env::get("WIN_DEBUG_LIBS"); + envs["CSC_KEY_PASSWORD"] = Env::get("CSC_KEY_PASSWORD"); // ssc variables - envs["SSC_CI"] = getEnv("SSC_CI"); - envs["SSC_RC"] = getEnv("SSC_RC"); - envs["SSC_RC_FILENAME"] = getEnv("SSC_RC_FILENAME"); - envs["SSC_ENV_FILENAME"] = getEnv("SSC_ENV_FILENAME"); + envs["SSC_CI"] = Env::get("SSC_CI"); + envs["SSC_RC"] = Env::get("SSC_RC"); + envs["SSC_RC_FILENAME"] = Env::get("SSC_RC_FILENAME"); + envs["SSC_ENV_FILENAME"] = Env::get("SSC_ENV_FILENAME"); if (envs["SOCKET_HOME_API"].size() == 0) { envs["SOCKET_HOME_API"] = trim(prefixFile("api")); @@ -6422,13 +6466,13 @@ int main (const int argc, const char* argv[]) { auto value = trim(entry.second); if (value.size() == 0) { - value = trim(getEnv(key)); + value = trim(Env::get(key)); } envs[key] = value; } else if (entry.first == "env") { for (const auto& key : parseStringList(entry.second)) { - auto value = trim(getEnv(key)); + auto value = trim(Env::get(key)); envs[key] = value; } } @@ -6442,13 +6486,13 @@ int main (const int argc, const char* argv[]) { auto value = trim(entry.second); if (value.size() == 0) { - value = trim(getEnv(key)); + value = trim(Env::get(key)); } envs[key] = value; } else if (entry.first == "env") { for (const auto& key : parseStringList(entry.second)) { - auto value = trim(getEnv(key)); + auto value = trim(Env::get(key)); envs[key] = value; } } @@ -6460,7 +6504,7 @@ int main (const int argc, const char* argv[]) { auto value = trim(entry.second); if (value.size() == 0) { - value = trim(getEnv(key)); + value = trim(Env::get(key)); } if (value.size() > 0) { diff --git a/src/cli/templates.hh b/src/cli/templates.hh index 8d0e371b1b..6c44644c4c 100644 --- a/src/cli/templates.hh +++ b/src/cli/templates.hh @@ -633,6 +633,10 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! /* Begin PBXFileReference section */ {{__ios_native_extensions_build_context_refs}} 171C1C2A2AC38A70005F587F /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; + 1790CE4D2AD78CCF00AA7E5B /* ini.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ini.hh; sourceTree = "<group>"; }; + 1790CE4E2AD78CCF00AA7E5B /* string.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = string.hh; sourceTree = "<group>"; }; + 1790CE4F2AD78CCF00AA7E5B /* config.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = config.hh; sourceTree = "<group>"; }; + 1790CE502AD78CCF00AA7E5B /* types.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = types.hh; sourceTree = "<group>"; }; 179989D12A867B260041EDC1 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; 17A7F8EE29358D180051D146 /* init.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = init.cc; sourceTree = "<group>"; }; 17A7F8F129358D180051D146 /* main.o */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.objfile"; path = main.o; sourceTree = "<group>"; }; @@ -677,6 +681,17 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 1790CE4C2AD78CCF00AA7E5B /* core */ = { + isa = PBXGroup; + children = ( + 1790CE4D2AD78CCF00AA7E5B /* ini.hh */, + 1790CE4E2AD78CCF00AA7E5B /* string.hh */, + 1790CE4F2AD78CCF00AA7E5B /* config.hh */, + 1790CE502AD78CCF00AA7E5B /* types.hh */, + ); + path = core; + sourceTree = "<group>"; + }; 17A7F8EF29358D180051D146 /* objects */ = { isa = PBXGroup; children = ( From c4ca6064ace2f3f29cb1fc98bbe4c9f6df84787e Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 22:28:09 -0400 Subject: [PATCH 179/256] refactor(app,desktop,ios,process,window): refactor to use single 'core.hh' --- src/app/app.cc | 2 +- src/desktop/main.cc | 36 +++++++++++++++++++++++++++--------- src/ios/main.mm | 13 ++++++------- src/process/process.hh | 6 ++++-- src/window/apple.mm | 2 +- src/window/options.hh | 2 +- src/window/window.hh | 19 ++++++++++++++----- 7 files changed, 54 insertions(+), 26 deletions(-) diff --git a/src/app/app.cc b/src/app/app.cc index 3f12e973bc..13e301b46c 100644 --- a/src/app/app.cc +++ b/src/app/app.cc @@ -192,7 +192,7 @@ namespace SSC { namespace SSC { static inline void alert (const SSC::WString &ws) { - MessageBoxA(nullptr, SSC::WStringToString(ws).c_str(), _TEXT("Alert"), MB_OK | MB_ICONSTOP); + MessageBoxA(nullptr, SSC::convertWStringToString(ws).c_str(), _TEXT("Alert"), MB_OK | MB_ICONSTOP); } static inline void alert (const SSC::String &s) { diff --git a/src/desktop/main.cc b/src/desktop/main.cc index facf9a305d..52108ea947 100644 --- a/src/desktop/main.cc +++ b/src/desktop/main.cc @@ -1,7 +1,14 @@ #include "../app/app.hh" +#include "../cli/cli.hh" +#include "../ipc/ipc.hh" +#include "../core/core.hh" #include "../process/process.hh" #include "../window/window.hh" -#include "../ipc/ipc.hh" + +#include <iostream> +#include <ostream> +#include <regex> +#include <span> // // A cross platform MAIN macro that @@ -52,6 +59,17 @@ SSC::String getNavigationError (const String &cwd, const String &value) { return SSC::String(""); } +inline const Vector<int> splitToInts (const String& s, const char& c) { + Vector<int> result; + String token; + std::istringstream ss(s); + + while (std::getline(ss, token, c)) { + result.push_back(std::stoi(token)); + } + return result; +} + // // the MAIN macro provides a cross-platform program entry point. // it makes argc and argv uniformly available. It provides "instanceId" @@ -185,11 +203,11 @@ MAIN { for (auto const &envKey : parseStringList(app.appData["build_env"])) { auto cleanKey = trim(envKey); - if (!hasEnv(cleanKey)) { + if (!Env::has(cleanKey)) { continue; } - auto envValue = getEnv(cleanKey.c_str()); + auto envValue = Env::get(cleanKey.c_str()); env << SSC::String( cleanKey + "=" + encodeURIComponent(envValue) + "&" @@ -250,15 +268,15 @@ MAIN { exitCode = stoi(message.get("value")); exit(exitCode); } else { - stdWrite(message.get("value"), false); + IO::write(message.get("value"), false); } }, - [](SSC::String const &out) { stdWrite(out, true); }, + [](SSC::String const &out) { IO::write(out, true); }, [](SSC::String const &code){ exit(std::stoi(code)); } ); if (cmd.size() == 0) { - stdWrite("No 'cmd' is provided for '" + platform.os + "' in socket.ini", true); + IO::write("No 'cmd' is provided for '" + platform.os + "' in socket.ini", true); exit(1); } @@ -531,7 +549,7 @@ MAIN { #if defined(__APPLE__) if (app.fromSSC) { debug("__EXIT_SIGNAL__=%d", exitCode); - notifyCli(); + CLI::notify(); } #endif window->exit(exitCode); @@ -556,7 +574,7 @@ MAIN { if (message.name == "application.getWindows") { const auto index = message.index; const auto window = windowManager.getWindow(index); - auto indices = SSC::splitToInts(value, ','); + auto indices = splitToInts(value, ','); if (indices.size() == 0) { for (auto w : windowManager.windows) { if (w != nullptr) { @@ -989,7 +1007,7 @@ MAIN { #if defined(__APPLE__) if (app_ptr->fromSSC) { debug("__EXIT_SIGNAL__=%d", 0); - notifyCli(); + CLI::notify(); } #endif diff --git a/src/ios/main.mm b/src/ios/main.mm index 9573d530bd..c98358056a 100644 --- a/src/ios/main.mm +++ b/src/ios/main.mm @@ -1,6 +1,7 @@ #include "../core/core.hh" #include "../ipc/ipc.hh" #include "../window/window.hh" +#include "../cli/cli.hh" using namespace SSC; @@ -83,9 +84,9 @@ - (void) userContentController: (WKUserContentController*) userContentController auto code = std::stoi(msg.get("value", "0")); if (code > 0) { - notifyCli(SIGTERM); + CLI::notify(SIGTERM); } else { - notifyCli(SIGUSR2); + CLI::notify(SIGUSR2); } } else { bridge->route([body UTF8String], nullptr, 0); @@ -171,8 +172,6 @@ - (BOOL) application: (UIApplication*) application { using namespace SSC; - platform.os = "ios"; - core = new Core; bridge = new IPC::Bridge(core); bridge->router.dispatchFunction = [=] (auto callback) { @@ -201,11 +200,11 @@ - (BOOL) application: (UIApplication*) application for (auto const &envKey : parseStringList(userConfig["build_env"])) { auto cleanKey = trim(envKey); - if (!hasEnv(cleanKey)) { + if (!Env::has(cleanKey)) { continue; } - auto envValue = getEnv(cleanKey.c_str()); + auto envValue = Env::get(cleanKey.c_str()); env << String( cleanKey + "=" + encodeURIComponent(envValue) + "&" @@ -231,7 +230,7 @@ - (BOOL) application: (UIApplication*) application // Note: you won't see any logs in the preload script before the // Web Inspector is opened - String preload = ToString(createPreload(opts)); + String preload = createPreload(opts); WKUserScript* initScript = [[WKUserScript alloc] initWithSource: [NSString stringWithUTF8String: preload.c_str()] diff --git a/src/process/process.hh b/src/process/process.hh index faeecf74e0..7f49313db5 100644 --- a/src/process/process.hh +++ b/src/process/process.hh @@ -1,6 +1,10 @@ #ifndef SSC_PROCESS_PROCESS_H #define SSC_PROCESS_PROCESS_H +#include <iostream> + +#include "../core/core.hh" + #ifndef WIFEXITED #define WIFEXITED(w) ((w) & 0x7f) #endif @@ -9,8 +13,6 @@ #define WEXITSTATUS(w) (((w) & 0xff00) >> 8) #endif -#include "../common.hh" - namespace SSC { struct ExecOutput { SSC::String output; diff --git a/src/window/apple.mm b/src/window/apple.mm index 67630b9600..d8e5a3d710 100644 --- a/src/window/apple.mm +++ b/src/window/apple.mm @@ -776,7 +776,7 @@ - (void) webView: (WKWebView*) webView WKUserContentController* controller = [config userContentController]; // Add preload script, normalizing the interface to be cross-platform. - SSC::String preload = ToString(createPreload(opts)); + SSC::String preload = createPreload(opts); WKUserScript* userScript = [WKUserScript alloc]; diff --git a/src/window/options.hh b/src/window/options.hh index 75b5ebd874..9cc96fcdcc 100644 --- a/src/window/options.hh +++ b/src/window/options.hh @@ -1,7 +1,7 @@ #ifndef SSC_WINDOW_OPTIONS_H #define SSC_WINDOW_OPTIONS_H -#include "../common.hh" +#include "../core/types.hh" namespace SSC { struct WindowOptions { diff --git a/src/window/window.hh b/src/window/window.hh index e8d01ddb4c..37f7376aad 100644 --- a/src/window/window.hh +++ b/src/window/window.hh @@ -1,8 +1,12 @@ #ifndef SSC_WINDOW_WINDOW_H #define SSC_WINDOW_WINDOW_H +#include <iostream> + #include "../ipc/ipc.hh" #include "../app/app.hh" +#include "../core/env.hh" + #include "options.hh" #ifndef SSC_MAX_WINDOWS @@ -90,6 +94,11 @@ namespace SSC { WINDOW_HINT_FIXED = 3 // Window size can not be changed by a user }; + struct ScreenSize { + int height = 0; + int width = 0; + }; + class Window { public: App& app; @@ -495,11 +504,11 @@ namespace SSC { for (auto const &envKey : parseStringList(opts.appData["build_env"])) { auto cleanKey = trim(envKey); - if (!hasEnv(cleanKey)) { + if (!Env::has(cleanKey)) { continue; } - auto envValue = getEnv(cleanKey.c_str()); + auto envValue = Env::get(cleanKey.c_str()); env << String( cleanKey + "=" + encodeURIComponent(envValue) + "&" @@ -509,11 +518,11 @@ namespace SSC { for (auto const &envKey : parseStringList(this->options.appData["build_env"])) { auto cleanKey = trim(envKey); - if (!hasEnv(cleanKey)) { + if (!Env::has(cleanKey)) { continue; } - auto envValue = getEnv(cleanKey.c_str()); + auto envValue = Env::get(cleanKey); env << String( cleanKey + "=" + encodeURIComponent(envValue) + "&" @@ -613,7 +622,7 @@ namespace SSC { }; #if defined(_WIN32) - using IEnvHandler = ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler; + using IEnvHandler = ICoreWebView2CreateCoreWebView2EnvCompletedHandler; using IConHandler = ICoreWebView2CreateCoreWebView2ControllerCompletedHandler; using INavHandler = ICoreWebView2NavigationCompletedEventHandler; using IRecHandler = ICoreWebView2WebMessageReceivedEventHandler; From fb3e0427ff02080b7fb376ff42fadc0e67d77cd3 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 22:28:49 -0400 Subject: [PATCH 180/256] refactor(cli): introduce 'cli.hh' --- src/cli/cli.hh | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/cli/cli.hh diff --git a/src/cli/cli.hh b/src/cli/cli.hh new file mode 100644 index 0000000000..85411d330e --- /dev/null +++ b/src/cli/cli.hh @@ -0,0 +1,27 @@ +#ifndef SSC_CLI_HH +#define SSC_CLI_HH + +#include "../core/platform.hh" +#include "../core/string.hh" +#include "../core/env.hh" + +#include <signal.h> + +namespace SSC::CLI { + inline void notify (int signal) { + #if !defined(_WIN32) + static auto ppid = Env::get("SSC_CLI_PID"); + static auto pid = ppid.size() > 0 ? std::stoi(ppid) : 0; + if (pid > 0) { + kill(pid, signal); + } + #endif + } + + inline void notify () { + #if !defined(_WIN32) + return notify(SIGUSR1); + #endif + } +} +#endif From 1a031ed64824198e8cbdb08d82631cdc8141df5d Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 22:32:20 -0400 Subject: [PATCH 181/256] refactor(src/android/window.cc): use 'SSC::Env::*' --- src/android/window.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/android/window.cc b/src/android/window.cc index 11708ce556..3459f0cf48 100644 --- a/src/android/window.cc +++ b/src/android/window.cc @@ -22,11 +22,11 @@ namespace SSC::android { for (auto const &var : parseStringList(this->config["build_env"])) { auto key = trim(var); - if (!hasEnv(key)) { + if (!Env::has(key)) { continue; } - auto value = getEnv(key.c_str()); + auto value = Env::get(key.c_str()); if (value.size() > 0) { stream << key << "=" << encodeURIComponent(value) << "&"; From 42ca02bfdae1f11e4641c66f1abe5ba795d298b3 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 22:32:44 -0400 Subject: [PATCH 182/256] refactor(src/init.cc): use core APIs --- src/init.cc | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/init.cc b/src/init.cc index 9100807b85..7589cbdb23 100644 --- a/src/init.cc +++ b/src/init.cc @@ -1,23 +1,25 @@ -#include "config.hh" +#include "core/config.hh" +#include "core/string.hh" +#include "core/types.hh" +#include "core/ini.hh" #if defined(__cplusplus) -#include <map> // These rely on project-specific, compile-time variables. namespace SSC { bool isDebugEnabled () { return DEBUG == 1; } - const std::map<std::string, std::string> getUserConfig () { + const Map getUserConfig () { #include "user-config-bytes.hh" // NOLINT - return parseINI(std::string( + return INI::parse(std::string( (const char*) __ssc_config_bytes, sizeof(__ssc_config_bytes) )); } const char* getDevHost () { - static const char* host = STR_VALUE(HOST); + static const char* host = CONVERT_TO_STRING(HOST); return host; } From 930fec3da9ebb4730c096cb60e70ccbd413d952b Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 22:33:00 -0400 Subject: [PATCH 183/256] fix(api/notification.js): fix typo --- api/notification.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/notification.js b/api/notification.js index e2765f1434..20828b32bf 100644 --- a/api/notification.js +++ b/api/notification.js @@ -876,7 +876,7 @@ export class Notification extends EventTarget { } hooks.onReady(() => { - if (os.host() === 'iphone-simulator' || os.host() === 'android-emulator') { + if (os.host() === 'iphonesimulator' || os.host() === 'android-emulator') { return } From 18c58b1d31af82b59d64fcd9b64acc8a1d24eeee Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 22:35:47 -0400 Subject: [PATCH 184/256] chore(src/common.hh): drop 'common.hh' --- src/common.hh | 1029 ------------------------------------------------- 1 file changed, 1029 deletions(-) delete mode 100644 src/common.hh diff --git a/src/common.hh b/src/common.hh deleted file mode 100644 index 13351be652..0000000000 --- a/src/common.hh +++ /dev/null @@ -1,1029 +0,0 @@ -#ifndef SSC_CORE_COMMON_H -#define SSC_CORE_COMMON_H - -#include "config.hh" - -// macOS/iOS -#if defined(__APPLE__) -#include <TargetConditionals.h> -#include <OSLog/OSLog.h> - -#if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR -#include <_types/_uint64_t.h> -#include <netinet/in.h> -#include <sys/un.h> -#else -#include <objc/objc-runtime.h> -#endif - -#if !defined(WAS_CODESIGNED) -#define WAS_CODESIGNED 0 -#endif - -#ifndef debug -#if !defined(SSC_CLI) -static os_log_t SSC_OS_LOG_DEBUG_BUNDLE = nullptr; -// wrap `os_log*` functions for global debugger -#define osdebug(format, fmt, ...) ({ \ - if (!SSC_OS_LOG_DEBUG_BUNDLE) { \ - static auto userConfig = SSC::getUserConfig(); \ - static auto bundleIdentifier = userConfig["meta_bundle_identifier"]; \ - SSC_OS_LOG_DEBUG_BUNDLE = os_log_create( \ - bundleIdentifier.c_str(), \ - "socket.runtime.debug" \ - ); \ - } \ - \ - auto string = [NSString stringWithFormat: @fmt, ##__VA_ARGS__]; \ - os_log_error( \ - SSC_OS_LOG_DEBUG_BUNDLE, \ - "%{public}s", \ - string.UTF8String \ - ); \ -}) -#else -#define osdebug(...) -#endif - -#define debug(format, ...) ({ \ - NSLog(@format, ##__VA_ARGS__); \ - osdebug("%{public}@", format, ##__VA_ARGS__); \ -}) -#endif -#endif - -// Linux -#if defined(__linux__) && !defined(__ANDROID__) -#ifndef debug -#define debug(format, ...) fprintf(stderr, format "\n", ##__VA_ARGS__) -#endif -#endif - -#if defined(_WIN32) && defined(DEBUG) -#define _WIN32_DEBUG 1 -#endif - -// Android (Linux) -#if defined(__linux__) && defined(__ANDROID__) -// Java Native Interface -// @see https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html -#include <jni.h> -#include <android/asset_manager.h> -#include <android/asset_manager_jni.h> -#include <android/log.h> - -#ifndef debug -#define debug(format, ...) \ - __android_log_print( \ - ANDROID_LOG_DEBUG, \ - "Console", \ - format, \ - ##__VA_ARGS__ \ - ); -#endif -#endif - -// Windows -#if defined(_WIN32) -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif - - -#undef _WINSOCKAPI_ -#define _WINSOCKAPI_ - -#include <WinSock2.h> -#include <windows.h> - -#include <dwmapi.h> -#include <io.h> -#include <tchar.h> -#include <wingdi.h> - -#include <signal.h> -#include <shlobj_core.h> -#include <shobjidl.h> - -#define isatty _isatty -#define fileno _fileno - -#ifndef debug -#define debug(format, ...) fprintf(stderr, format "\n", ##__VA_ARGS__) -#endif -#endif - -#include <errno.h> -#include <math.h> -#include <stdlib.h> -#include <stdio.h> - -#if !defined(_WIN32) -#include <arpa/inet.h> -#include <ifaddrs.h> -#include <sys/socket.h> -#include <sys/types.h> -#include <sys/wait.h> -#include <unistd.h> -#endif - -#include <array> -#include <chrono> -#include <cstdint> -#include <iostream> -#include <exception> -#include <filesystem> -#include <fstream> -#include <functional> -#include <map> -#include <mutex> -#include <queue> -#include <regex> -#include <span> -#include <sstream> -#include <string> -#include <thread> -#include <vector> - -#if defined(_WIN32) -#define SHARED_OBJ_EXT ".dll" -#else -#define SHARED_OBJ_EXT ".so" -#endif - - - /* -#if !DEBUG -#ifdef debug -#undef debug -#endif -#define debug(format, ...) -#endif -*/ -#define TO_STR(arg) #arg -#define STR_VALUE(arg) TO_STR(arg) - -#define IN_URANGE(c, a, b) ( \ - (unsigned char) c >= (unsigned char) a && \ - (unsigned char) c <= (unsigned char) b \ -) - -#define IMAX_BITS(m) ((m)/((m) % 255+1) / 255 % 255 * 8 + 7-86 / ((m) % 255+12)) -#define RAND_MAX_WIDTH IMAX_BITS(RAND_MAX) - -#define ToWString(string) WString(L##string) -#define ToString(string) String(string) - -namespace SSC { -#if !defined(__APPLE__) || (defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR) - namespace fs = std::filesystem; - using Path = fs::path; -#endif - - using String = std::string; - using StringStream = std::stringstream; - using WString = std::wstring; - using WStringStream = std::wstringstream; - - template <typename T> using Queue = std::queue<T>; - template <typename T> using Vector = std::vector<T>; - - using ExitCallback = std::function<void(int code)>; - using Map = std::map<String, String>; - using Mutex = std::recursive_mutex; - using Lock = std::lock_guard<Mutex>; - using MessageCallback = std::function<void(const String)>; - using Thread = std::thread; - - inline const auto VERSION_FULL_STRING = ToString(STR_VALUE(SSC_VERSION) " (" STR_VALUE(SSC_VERSION_HASH) ")"); - inline const auto VERSION_HASH_STRING = ToString(STR_VALUE(SSC_VERSION_HASH)); - inline const auto VERSION_STRING = ToString(STR_VALUE(SSC_VERSION)); - - inline const auto DEFAULT_SSC_RC_FILENAME = String(".sscrc"); - inline const auto DEFAULT_SSC_ENV_FILENAME = String(".ssc.env"); - - inline String encodeURIComponent (const String& sSrc); - inline String decodeURIComponent (const String& sSrc); - inline String trim (String str); - - inline WString StringToWString (const String& s) { - WString temp(s.length(), L' '); - std::copy(s.begin(), s.end(), temp.begin()); - return temp; - } - - inline WString StringToWString (const WString& s) { - return s; - } - - inline String WStringToString (const WString& s) { - String temp(s.length(), ' '); - std::copy(s.begin(), s.end(), temp.begin()); - return temp; - } - - inline String WStringToString (const String& s) { - return s; - } - - #if defined(_WIN32) - SSC::String FormatError(DWORD error, SSC::String source); - #endif - - // - // Reporting on the platform. - // - static struct { - #if defined(__x86_64__) || defined(_M_X64) - const String arch = "x86_64"; - #elif defined(__aarch64__) || defined(_M_ARM64) - const String arch = "arm64"; - #elif defined(__i386__) && !defined(__ANDROID__) - #error Socket is not supported on i386. - #else - const String arch = "unknown"; - #endif - - #if defined(_WIN32) - const String os = "win32"; - bool mac = false; - bool ios = false; - bool win = true; - bool linux = false; - bool unix = false; - - #elif defined(__APPLE__) - bool win = false; - bool linux = false; - - #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR - String os = "ios"; - bool ios = true; - bool mac = false; - #else - String os = "mac"; - bool ios = false; - bool mac = true; - #endif - - #if defined(__unix__) || defined(unix) || defined(__unix) - bool unix = true; - #else - bool unix = false; - #endif - - #elif defined(__linux__) - #undef linux - #ifdef __ANDROID__ - const String os = "android"; - #else - const String os = "linux"; - #endif - - bool mac = false; - bool ios = false; - bool win = false; - bool linux = true; - - #if defined(__unix__) || defined(unix) || defined(__unix) - bool unix = true; - #else - bool unix = false; - #endif - - #elif defined(__FreeBSD__) - const String os = "freebsd"; - bool mac = false; - bool ios = false; - bool win = false; - bool linux = false; - - #if defined(__unix__) || defined(unix) || defined(__unix) - bool unix = true; - #else - bool unix = false; - #endif - - #elif defined(BSD) - const String os = "openbsd"; - bool ios = false; - bool mac = false; - bool win = false; - bool linux = false; - - #if defined(__unix__) || defined(unix) || defined(__unix) - bool unix = true; - #else - bool unix = false; - #endif - - #endif - } platform; - - inline const Vector<String> splitc (const String& s, const char& c) { - String buff; - Vector<String> vec; - - for (auto n : s) { - if (n != c) { - buff += n; - } else if (n == c) { - vec.push_back(buff); - buff = ""; - } - } - - vec.push_back(buff); - - return vec; - } - - inline const String join ( - const Vector<String>& vector, - const String& separator - ) { - StringStream joined; - auto missing = vector.size(); - for (const auto& item : vector) { - joined << item; - if (--missing > 0) { - joined << separator << " "; - } - } - - return trim(joined.str()); - } - - inline size_t decodeUTF8 (char *output, const char *input, size_t length) { - unsigned char cp = 0; // code point - unsigned char lower = 0x80; - unsigned char upper = 0xBF; - - int x = 0; // cp needed - int y = 0; // cp seen - int size = 0; // output size - - for (int i = 0; i < length; ++i) { - auto b = (unsigned char) input[i]; - - if (b == 0) { - output[size++] = 0; - continue; - } - - if (x == 0) { - // 1 byte - if (IN_URANGE(b, 0x00, 0x7F)) { - output[size++] = b; - continue; - } - - if (!IN_URANGE(b, 0xC2, 0xF4)) { - break; - } - - // 2 byte - if (IN_URANGE(b, 0xC2, 0xDF)) { - x = 1; - cp = b - 0xC0; - } - - // 3 byte - if (IN_URANGE(b, 0xE0, 0xEF)) { - if (b == 0xE0) { - lower = 0xA0; - } else if (b == 0xED) { - upper = 0x9F; - } - - x = 2; - cp = b - 0xE0; - } - - // 4 byte - if (IN_URANGE(b, 0xF0, 0xF4)) { - if (b == 0xF0) { - lower = 0x90; - } else if (b == 0xF4) { - upper = 0x8F; - } - - x = 3; - cp = b - 0xF0; - } - - cp = cp * pow(64, x); - continue; - } - - if (!IN_URANGE(b, lower, upper)) { - lower = 0x80; - upper = 0xBF; - - // revert - cp = 0; - x = 0; - y = 0; - i--; - continue; - } - - lower = 0x80; - upper = 0xBF; - y++; - cp += (b - 0x80) * pow(64, x - y); - - if (y != x) { - continue; - } - - output[size++] = cp; - // continue to next - cp = 0; - x = 0; - y = 0; - } - - return size; - } - - inline String replace (const String& src, const String& re, const String& val) { - return std::regex_replace(src, std::regex(re), val); - } - - inline String& replaceAll (String& src, String const& from, String const& to) { - size_t start = 0; - size_t index; - - while ((index = src.find(from, start)) != String::npos) { - src.replace(index, from.size(), to); - start = index + to.size(); - } - return src; - } - - // - // Helper functions... - // - inline const Vector<String> split (const String& s, const char& c) { - String buff; - Vector<String> vec; - - for (auto n : s) { - if(n != c) { - buff += n; - } else if (n == c && buff != "") { - vec.push_back(buff); - buff = ""; - } - } - - if (!buff.empty()) vec.push_back(buff); - - return vec; - } - - inline const Vector<int> splitToInts (const String& s, const char& c) { - std::vector<int> result; - std::istringstream ss(s); - std::string token; - while (std::getline(ss, token, c)) { - result.push_back(std::stoi(token)); - } - return result; - } - - inline String trim (String str) { - str.erase(0, str.find_first_not_of(" \r\n\t")); - str.erase(str.find_last_not_of(" \r\n\t") + 1); - return str; - } - - inline String tmpl (const String s, Map pairs) { - String output = s; - - for (auto item : pairs) { - auto key = String("[{]+(" + item.first + ")[}]+"); - auto value = item.second; - output = std::regex_replace(output, std::regex(key), value); - } - - return output; - } - - inline uint64_t rand64 (void) { - uint64_t r = 0; - static bool init = false; - - if (!init) { - init = true; - srand(time(0)); - } - - for (int i = 0; i < 64; i += RAND_MAX_WIDTH) { - r <<= RAND_MAX_WIDTH; - r ^= (unsigned) rand(); - } - return r; - } - - inline bool hasEnv (const char* variableName) { - static auto appData = getUserConfig(); - - if (appData[String("env_") + variableName].size() > 0) { - return true; - } - - #if defined(_WIN32) - char* value = nullptr; - size_t size = 0; - auto result = _dupenv_s(&value, &size, variableName); - - if (value && value[0] == '\0') { - free(value); - return false; - } - - free(value); - - if (size == 0 || result != 0) { - return false; - } - #else - auto value = getenv(variableName); - - if (value == nullptr || value[0] == '\0') { - return false; - } - #endif - - return true; - } - - inline bool hasEnv (const String& variableName) { - return hasEnv(variableName.c_str()); - } - - inline String getEnv (const char* variableName) { - static auto appData = getUserConfig(); - - if (appData[String("env_") + variableName].size() > 0) { - return appData[String("env_") + variableName]; - } - - #if defined(_WIN32) - char* variableValue = nullptr; - std::size_t valueSize = 0; - auto query = _dupenv_s(&variableValue, &valueSize, variableName); - - String result; - if(query == 0 && variableValue != nullptr && valueSize > 0) { - result.assign(variableValue, valueSize - 1); - free(variableValue); - } - - return result; - #else - auto v = getenv(variableName); - - if (v != nullptr) { - return String(v); - } - - return String(""); - #endif - } - - inline String getEnv (const String& variableName) { - return getEnv(variableName.c_str()); - } - - inline String getEnv (const String& variableName, const String& defaultValue) { - const auto value = getEnv(variableName); - if (value.size() == 0) { - return defaultValue; - } - - return value; - } - - inline auto setEnv (const String& k, const String& v) { - #if defined(_WIN32) - return _putenv((k + "=" + v).c_str()); - #else - setenv(k.c_str(), v.c_str(), 1); - #endif - } - - inline auto setEnv (const char* s) { - #if defined(_WIN32) - return _putenv(s); - #else - auto parts = split(String(s), '='); - setEnv(parts[0], parts[1]); - #endif - } - - inline void notifyCli (int signal) { - #if !defined(_WIN32) - static auto ppid = getEnv("SSC_CLI_PID"); - static auto pid = ppid.size() > 0 ? std::stoi(ppid) : 0; - if (pid > 0) { - kill(pid, signal); - } - #endif - } - - inline void notifyCli () { - #if !defined(_WIN32) - return notifyCli(SIGUSR1); - #endif - } - - inline void stdWrite (const String &str, bool isError) { - static const auto IN_GITHUB_ACTIONS_CI = getEnv("GITHUB_ACTIONS_CI").size() > 0; - auto& stream = isError ? std::cerr : std::cout; - stream << str; - - #if defined(_WIN32) - if (IN_GITHUB_ACTIONS_CI) { - notifyCli(); - return; - } - #endif - - stream << std::endl; - notifyCli(); - } - - #if !defined(__APPLE__) || (defined(__APPLE__) && !TARGET_OS_IPHONE && !TARGET_IPHONE_SIMULATOR) - inline String readFile (fs::path path) { - if (fs::is_directory(path)) { - stdWrite("WARNING: trying to read a directory as a file: " + path.string(), true); - return ""; - } - - std::ifstream stream(path.c_str()); - String content; - auto buffer = std::istreambuf_iterator<char>(stream); - auto end = std::istreambuf_iterator<char>(); - content.assign(buffer, end); - stream.close(); - return content; - } - - inline void writeFile (fs::path path, String s) { - std::ofstream stream(path.string()); - stream << s; - stream.close(); - } - - inline void appendFile (fs::path path, String s) { - std::ofstream stream; - stream.open(path.string(), std::ios_base::app); - stream << s; - stream.close(); - } - #endif - - inline Map& extendMap (Map& dst, const Map& src) { - for (const auto& tuple : src) { - dst[tuple.first] = tuple.second; - } - return dst; - } - - inline Map parseINI (String source) { - Vector<String> entries = split(source, '\n'); - String prefix = ""; - Map settings = {}; - - for (auto entry : entries) { - entry = trim(entry); - - // handle a variety of comment styles - if (entry[0] == ';' || entry[0] == '#') { - continue; - } - - if (entry.starts_with("[") && entry.ends_with("]")) { - if (entry.starts_with("[.") && entry.ends_with("]")) { - prefix += entry.substr(2, entry.length() - 3); - } else { - prefix = entry.substr(1, entry.length() - 2); - } - - prefix = replace(prefix, "\\.", "_"); - if (prefix.size() > 0) { - prefix += "_"; - } - - continue; - } - - auto index = entry.find_first_of('='); - - if (index >= 0 && index <= entry.size()) { - auto key = trim(prefix + entry.substr(0, index)); - auto value = trim(entry.substr(index + 1)); - - // trim quotes from quoted strings - size_t closing_quote_index = -1; - bool quoted_value = false; - if (value[0] == '"') { - closing_quote_index = value.find_first_of('"', 1); - if (closing_quote_index != std::string::npos) { - quoted_value = true; - value = trim(value.substr(1, closing_quote_index - 1)); - } - } - - if (!quoted_value) { - // ignore comments within quoted part of value - auto i = value.find_first_of(';'); - auto j = value.find_first_of('#'); - - if (i > 0) { - value = trim(value.substr(0, i)); - } - else if (j > 0) { - value = trim(value.substr(0, j)); - } - } - - if (key.ends_with("[]")) { - key = trim(key.substr(0, key.size() - 2)); - if (settings[key].size() > 0) { - settings[key] += " " + value; - } else { - settings[key] = value; - } - } else { - settings[key] = value; - } - } - } - - return settings; - } - - // - // IPC Message parser for the middle end - // TODO possibly harden data validation. - // - class Parse { - Map args; - public: - Parse(const String&); - int index = -1; - String value = ""; - String name = ""; - String uri = ""; - String get(const String&) const; - String get(const String&, const String) const; - const char * c_str () const { - return this->uri.c_str(); - } - }; - - struct ScreenSize { - int height = 0; - int width = 0; - }; - - // - // cmd: `ipc://id?p1=v1&p2=v2&...\0` - // - inline Parse::Parse (const String& s) { - String str = s; - uri = str; - - // bail if missing protocol prefix - if (str.find("ipc://") == -1) return; - - // bail if malformed - if (str.compare("ipc://") == 0) return; - if (str.compare("ipc://?") == 0) return; - - String query; - String path; - - auto raw = split(str, '?'); - path = raw[0]; - if (raw.size() > 1) query = raw[1]; - - auto parts = split(path, '/'); - if (parts.size() >= 1) name = parts[1]; - - if (raw.size() != 2) return; - auto pairs = split(raw[1], '&'); - - for (auto& rawPair : pairs) { - auto pair = split(rawPair, '='); - if (pair.size() <= 1) continue; - - if (pair[0].compare("index") == 0) { - try { - index = std::stoi(pair[1].size() > 0 ? pair[1] : "0"); - } catch (...) { - std::cout << "Warning: received non-integer index" << std::endl; - } - } - - args[pair[0]] = pair[1]; - } - } - - inline String Parse::get(const String& s) const { - return args.count(s) ? args.at(s): ""; - } - - inline String Parse::get(const String& s, const String fallback) const { - return args.count(s) ? args.at(s) : fallback; - } - - // - // All ipc uses a URI schema, so all ipc data needs to be - // encoded as a URI component. This prevents escaping the - // protocol. - // - static const signed char HEX2DEC[256] = { - /* 0 1 2 3 4 5 6 7 8 9 A B C D E F */ - /* 0 */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - /* 1 */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - /* 2 */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - /* 3 */ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,-1,-1, -1,-1,-1,-1, - - /* 4 */ -1,10,11,12, 13,14,15,-1, -1,-1,-1,-1, -1,-1,-1,-1, - /* 5 */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - /* 6 */ -1,10,11,12, 13,14,15,-1, -1,-1,-1,-1, -1,-1,-1,-1, - /* 7 */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - - /* 8 */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - /* 9 */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - /* A */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - /* B */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - - /* C */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - /* D */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - /* E */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - /* F */ -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1 - }; - - inline std::string stringToHex (const std::string& input) { - static const char set[] = "0123456789ABCDEF"; - - std::string output; - output.reserve(input.length() * 2); - - for (unsigned char c : input) { - output.push_back(set[c >> 4]); - output.push_back(set[c & 15]); - } - - return output; - } - - inline std::string hexToString (const std::string& input) { - const auto len = input.length(); - - std::string output; - output.reserve(len / 2); - - for (auto it = input.begin(); it != input.end();) { - int hi = HEX2DEC[(unsigned char) *it++]; - int lo = HEX2DEC[(unsigned char) *it++]; - output.push_back(hi << 4 | lo); - } - - return output; - } - - inline String decodeURIComponent (const String& sSrc) { - - // Note from RFC1630: "Sequences which start with a percent sign - // but are not followed by two hexadecimal characters (0-9, A-F) are reserved - // for future extension" - - auto s = replace(sSrc, "\\+", " "); - const unsigned char* pSrc = (const unsigned char *) s.c_str(); - const int SRC_LEN = (int) sSrc.length(); - const unsigned char* const SRC_END = pSrc + SRC_LEN; - const unsigned char* const SRC_LAST_DEC = SRC_END - 2; - - char* const pStart = new char[SRC_LEN]; - char* pEnd = pStart; - - while (pSrc < SRC_LAST_DEC) { - if (*pSrc == '%') { - char dec1, dec2; - if (-1 != (dec1 = HEX2DEC[*(pSrc + 1)]) - && -1 != (dec2 = HEX2DEC[*(pSrc + 2)])) { - - *pEnd++ = (dec1 << 4) + dec2; - pSrc += 3; - continue; - } - } - *pEnd++ = *pSrc++; - } - - // the last 2- chars - while (pSrc < SRC_END) { - *pEnd++ = *pSrc++; - } - - String sResult(pStart, pEnd); - delete [] pStart; - return sResult; - } - - static const char SAFE[256] = { - /* 0 1 2 3 4 5 6 7 8 9 A B C D E F */ - /* 0 */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, - /* 1 */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, - /* 2 */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, - /* 3 */ 1,1,1,1, 1,1,1,1, 1,1,0,0, 0,0,0,0, - - /* 4 */ 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1, - /* 5 */ 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0, - /* 6 */ 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1, - /* 7 */ 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0, - - /* 8 */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, - /* 9 */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, - /* A */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, - /* B */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, - - /* C */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, - /* D */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, - /* E */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, - /* F */ 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0 - }; - - inline String encodeURIComponent (const String& sSrc) { - const char DEC2HEX[16 + 1] = "0123456789ABCDEF"; - const unsigned char* pSrc = (const unsigned char*) sSrc.c_str(); - const int SRC_LEN = (int) sSrc.length(); - unsigned char* const pStart = new unsigned char[SRC_LEN* 3]; - unsigned char* pEnd = pStart; - const unsigned char* const SRC_END = pSrc + SRC_LEN; - - for (; pSrc < SRC_END; ++pSrc) { - if (SAFE[*pSrc]) { - *pEnd++ = *pSrc; - } else { - // escape this char - *pEnd++ = '%'; - *pEnd++ = DEC2HEX[*pSrc >> 4]; - *pEnd++ = DEC2HEX[*pSrc & 0x0F]; - } - } - - String sResult((char*) pStart, (char*) pEnd); - delete [] pStart; - return sResult; - } - - inline auto toBytes (uint64_t n) { - std::array<uint8_t, 8> bytes; - // big endian, network order - bytes[0] = n >> 8*7; - bytes[1] = n >> 8*6; - bytes[2] = n >> 8*5; - bytes[3] = n >> 8*4; - bytes[4] = n >> 8*3; - bytes[5] = n >> 8*2; - bytes[6] = n >> 8*1; - bytes[7] = n >> 8*0; - return bytes; - } - - inline auto msleep (uint64_t n) { - std::this_thread::yield(); - std::this_thread::sleep_for(std::chrono::milliseconds(n)); - } - - inline Vector<String> parseStringList (const String& string, Vector<char> separators) { - auto list = Vector<String>(); - for (const auto& separator : separators) { - for (const auto& part: split(string, separator)) { - if (std::find(list.begin(), list.end(), part) == list.end()) { - list.push_back(part); - } - } - } - - return list; - } - - inline Vector<String> parseStringList (const String& string, const char separator) { - return split(string, separator); - } - - inline Vector<String> parseStringList (const String& string) { - return parseStringList(string, { ' ', ',' }); - } -} - -#endif // SSC_H From 6dcb07e66f5c1e7b080269f71a11a79e1f9b9652 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 22:36:11 -0400 Subject: [PATCH 185/256] chore(bin/install.sh): include 'core/core.hh' in feature test --- bin/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/install.sh b/bin/install.sh index d73cd59873..feb89315be 100755 --- a/bin/install.sh +++ b/bin/install.sh @@ -880,7 +880,7 @@ function _check_compiler_features { cflags+=("-I$root") $CXX "${cflags[@]}" "${ldflags[@]}" - -o /dev/null >/dev/null << EOF_CC - #include "src/common.hh" + #include "src/core/core.hh" int main () { return 0; } EOF_CC From 39a1cc1eba2448b1e7ef2f31eeafa6f9a973f135 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 22:36:27 -0400 Subject: [PATCH 186/256] chore(src/cli/templates.hh): remove 'common.hh' from ios project --- src/cli/templates.hh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/cli/templates.hh b/src/cli/templates.hh index 6c44644c4c..a868d41c31 100644 --- a/src/cli/templates.hh +++ b/src/cli/templates.hh @@ -643,7 +643,6 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! 17A7F8F329358D430051D146 /* libsocket-runtime.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libsocket-runtime.a"; path = "lib/libsocket-runtime.a"; sourceTree = "<group>"; }; 17A7F8F429358D430051D146 /* libuv.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libuv.a; path = lib/libuv.a; sourceTree = "<group>"; }; 17C230B928E9398700301440 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; - 17E73FDA28FCC9320087604F /* common.hh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = common.hh; sourceTree = "<group>"; }; 17E73FEE28FCD3360087604F /* libuv-ios.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = "libuv-ios.a"; path = "lib/libuv-ios.a"; sourceTree = "<group>"; }; 290F7F86276BC2B000486988 /* lib */ = {isa = PBXFileReference; lastKnownFileType = folder; path = lib; sourceTree = "<group>"; }; 29124C4A27613369001832A0 /* {{build_name}}.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "{{build_name}}.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -711,9 +710,9 @@ constexpr auto gXCodeProject = R"ASCII(// !$*UTF8*$! 29124C4127613369001832A0 = { isa = PBXGroup; children = ( + 1790CE512AD792B600AA7E5B /* core */, 17A7F8EE29358D180051D146 /* init.cc */, 17A7F8EF29358D180051D146 /* objects */, - 17E73FDA28FCC9320087604F /* common.hh */, 290F7F86276BC2B000486988 /* lib */, 294A3C9027677424007B5B9A /* socket.entitlements */, 294A3C842764EAB7007B5B9A /* ui */, From 00276e5a3e82279d116311e7967e424a2dbbc9a0 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 11 Oct 2023 22:42:08 -0400 Subject: [PATCH 187/256] fix(src/window/window.hh): fix typo --- src/window/window.hh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/window/window.hh b/src/window/window.hh index 37f7376aad..c91ffe3ea9 100644 --- a/src/window/window.hh +++ b/src/window/window.hh @@ -622,7 +622,7 @@ namespace SSC { }; #if defined(_WIN32) - using IEnvHandler = ICoreWebView2CreateCoreWebView2EnvCompletedHandler; + using IEnvHandler = ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler; using IConHandler = ICoreWebView2CreateCoreWebView2ControllerCompletedHandler; using INavHandler = ICoreWebView2NavigationCompletedEventHandler; using IRecHandler = ICoreWebView2WebMessageReceivedEventHandler; From 8056adbe98aa2847bde40bc2929084cda46aad69 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Thu, 12 Oct 2023 16:40:37 +0200 Subject: [PATCH 188/256] fix(src): fix linux core build --- src/cli/cli.cc | 2 ++ src/core/codec.cc | 1 + src/core/platform.hh | 2 +- src/core/string.cc | 6 +++++- src/core/string.hh | 2 ++ src/window/linux.cc | 2 +- 6 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 38a80bf68a..5bee0413d4 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -33,6 +33,7 @@ #include <sys/types.h> #include <algorithm> +#include <chrono> #include <filesystem> #include <fstream> #include <iostream> @@ -45,6 +46,7 @@ #include "../process/process.hh" #include "templates.hh" +#include "cli.hh" #ifndef CMD_RUNNER #define CMD_RUNNER diff --git a/src/core/codec.cc b/src/core/codec.cc index e3d9d8ffd8..e164bb5ec5 100644 --- a/src/core/codec.cc +++ b/src/core/codec.cc @@ -1,5 +1,6 @@ #include "codec.hh" #include "string.hh" +#include <math.h> #define UNSIGNED_IN_RANGE(value, min, max) ( \ (unsigned char) (value) >= (unsigned char) (min) && \ diff --git a/src/core/platform.hh b/src/core/platform.hh index 750477df87..96b4dfb202 100644 --- a/src/core/platform.hh +++ b/src/core/platform.hh @@ -28,7 +28,7 @@ #else #include <Cocoa/Cocoa.h> #include <objc/objc-runtime.h> -#endif +#endif // `TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR` #endif // `__APPLE__` // Linux diff --git a/src/core/string.cc b/src/core/string.cc index 1acb87b3ad..4062e0a190 100644 --- a/src/core/string.cc +++ b/src/core/string.cc @@ -2,8 +2,12 @@ #include <regex> namespace SSC { + String replace (const String& source, const std::regex& regex, const String& value) { + return std::regex_replace(source, regex, value); + } + String replace (const String& source, const String& regex, const String& value) { - return std::regex_replace(source, std::regex(regex), value); + return replace(source, std::regex(regex), value); } String tmpl (const String& source, const Map& variables) { diff --git a/src/core/string.hh b/src/core/string.hh index c80dcaee1a..762196a55d 100644 --- a/src/core/string.hh +++ b/src/core/string.hh @@ -2,6 +2,7 @@ #define SSC_CORE_STRING_HH #include "types.hh" +#include <regex> /** * Converts a literal expression to an inline string: @@ -13,6 +14,7 @@ namespace SSC { // transform String replace (const String& source, const String& regex, const String& value); + String replace (const String& source, const std::regex& regex, const String& value); String tmpl (const String& source, const Map& variables); String trim (String source); diff --git a/src/window/linux.cc b/src/window/linux.cc index 67ddece97c..24933cbfae 100644 --- a/src/window/linux.cc +++ b/src/window/linux.cc @@ -767,7 +767,7 @@ namespace SSC { this ); - String preload = ToString(createPreload(opts)); + String preload = createPreload(opts); WebKitUserContentManager *manager = webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(webview)); From 916b6414bd783347fdfe0b9b170dc021e47a52b6 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Thu, 12 Oct 2023 17:54:28 +0200 Subject: [PATCH 189/256] refactor(bin): generate 'pkgconfig' files --- bin/build-runtime-library.sh | 4 + bin/functions.sh | 2 +- bin/generate-socket-runtime-pkg-config.sh | 167 ++++++++++++++++++++ bin/install.sh | 14 +- bin/ldflags.sh | 4 +- bin/mush.sh | 176 ++++++++++++++++++++++ bin/publish-npm-modules.sh | 4 + socket-runtime.pc.in | 7 + 8 files changed, 370 insertions(+), 8 deletions(-) create mode 100755 bin/generate-socket-runtime-pkg-config.sh create mode 100755 bin/mush.sh create mode 100644 socket-runtime.pc.in diff --git a/bin/build-runtime-library.sh b/bin/build-runtime-library.sh index 72ac3970f5..ca5872ddd3 100755 --- a/bin/build-runtime-library.sh +++ b/bin/build-runtime-library.sh @@ -237,6 +237,10 @@ function main () { fi fi + if [[ "$host" = "Linux" ]] && [[ "$platform" = "desktop" ]]; then + "$root/bin/generate-socket-runtime-pkg-config.sh" + fi + if [[ "$platform" == "android" ]]; then # This is a sanity check to confirm that the static_library is > 8 bytes # If an empty ${objects[@]} is provided to ar, it will still spit out a header without an error code. diff --git a/bin/functions.sh b/bin/functions.sh index ea5cb18ce5..17f57e6c6c 100755 --- a/bin/functions.sh +++ b/bin/functions.sh @@ -92,7 +92,7 @@ function quiet () { if [ -n "$VERBOSE" ]; then echo "$command" "$@" "$command" "$@" - else + else "$command" "$@" > /dev/null 2>&1 fi diff --git a/bin/generate-socket-runtime-pkg-config.sh b/bin/generate-socket-runtime-pkg-config.sh new file mode 100755 index 0000000000..87c5eea46f --- /dev/null +++ b/bin/generate-socket-runtime-pkg-config.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +# vim: set syntax=bash: + +declare root="$(cd "$(dirname "$(dirname "${BASH_SOURCE[0]}")")" && pwd)" + +source "$root/bin/mush.sh" +source "$root/bin/functions.sh" +source "$root/bin/android-functions.sh" + +declare platform="desktop" +declare host="$(host_os)" +declare arch="$(host_arch)" + +if (( TARGET_OS_IPHONE )); then + arch="arm64" + platform="iPhoneOS" +elif (( TARGET_IPHONE_SIMULATOR )); then + arch="x86_64" + platform="iPhoneSimulator" +elif (( TARGET_OS_ANDROID )); then + arch="aarch64" + platform="android" +elif (( TARGET_ANDROID_EMULATOR )); then + arch="x86_64" + platform="android" +fi + +while (( $# > 0 )); do + declare arg="$1"; shift + if [[ "$arg" = "--arch" ]]; then + arch="$1"; shift; continue + fi + + if [[ "$arg" = "--force" ]] || [[ "$arg" = "-f" ]]; then + pass_force="$arg" + force=1; continue + fi + + if [[ "$arg" = "--platform" ]]; then + if [[ "$1" = "ios" ]] || [[ "$1" = "iPhoneOS" ]] || [[ "$1" = "iphoneos" ]]; then + arch="arm64" + platform="iPhoneOS"; + export TARGET_OS_IPHONE=1 + elif [[ "$1" = "ios-simulator" ]] || [[ "$1" = "iPhoneSimulator" ]] || [[ "$1" = "iphonesimulator" ]]; then + [[ -z "$arch" ]] && arch="x86_64" + platform="iPhoneSimulator"; + export TARGET_IPHONE_SIMULATOR=1 + elif [[ "$1" = "android" ]] || [[ "$1" = "android-emulator" ]]; then + platform="android"; + export TARGET_OS_ANDROID=1 + else + platform="$1"; + fi + shift + continue + fi + + # Don't rebuild if header mtimes are newer than .o files - Be sure to manually delete affected assets as required + if [[ "$arg" == "--ignore-header-mtimes" ]]; then + ignore_header_mtimes=1; continue + fi + + args+=("$arg") +done + +declare input="$root/socket-runtime.pc.in" +declare output="$root/build/$arch-$platform/pkgconfig/socket-runtime.pc" + +mkdir -p "$(dirname "$output")" + +declare version="$(cat "$root/VERSION.txt")" +declare lib_directory="$root/build/$arch-$platform/lib" +declare include_directory="$root/build/$arch-$platform/include" + +declare ldflags=() +declare dependencies=() +declare cflags=( + "-0s" + "-std=c++2a" + "-fvisibility=hidden" +) + +if [ "$platform" == "iPhoneOS" ]; then + platform="ios" +elif [ "$platform" == "iPhoneSimulator" ]; then + platform="ios-simulator" +fi + +if [ "$host" == "Linux" ]; then + if [ "$platform" == "desktop" ]; then + if [[ "$(basename "$CXX")" =~ clang ]]; then + cflags+=("-stdlib=libstdc++") + cflags+=("-Wno-unused-command-line-argument") + fi + cflags+=("-fPIC") + ldflags+=("-ldl") + dependencies+=("gtk+-3.0" "webkit2gtk-4.1") + fi +elif [ "$host" == "Win32" ]; then + if [ "$platform" == "desktop" ]; then + if [[ "$(basename "$CXX")" =~ clang ]]; then + cflags+=("-Wno-unused-command-line-argument") + cflags+=("-stdlib=libstdc++") + fi + + # https://learn.microsoft.com/en-us/cpp/c-runtime-library/crt-library-features?view=msvc-170 + # Because we can't pass /MT[d] directly, we have to manually set the flags + cflags+=( + "-D_MT" + "-D_DLL" + "-DWIN32" + "-DWIN32_LEAN_AND_MEAN" + "-Xlinker" "/NODEFAULTLIB:libcmt" + "-Wno-nonportable-include-path" + ) + fi +elif [ "$host" == "Darwin" ]; then + if [ "$platform" == "desktop" ]; then + cflags+=("-ObjC++") + cflags+=("-fPIC") + fi +fi + +if [ "$platform" == "android" ]; then + android_fte > /dev/null 2>&1 + cflags+=("-DANDROID -pthreads -fexceptions -fPIC -frtti -fsigned-char -D_FILE_OFFSET_BITS=64 -Wno-invalid-command-line-argument -Wno-unused-command-line-argument") + cflags+=("-I$(dirname $NDK_BUILD)/sources/cxx-stl/llvm-libc++/include") + cflags+=("-I$(dirname $NDK_BUILD)/sources/cxx-stl/llvm-libc++abi/include") +fi + +if [ "$platform" == "ios" ] || [ "$platform" == "ios-simulator" ]; then + if [ "$host" != "Darwin" ]; then + echo "error: Cannot generate pkgconfig file for iPhoneOS or iPhoneSimulator on '$host'" >&2 + exit 1 + fi + + if [ "$platform" == "ios" ]; then + ios_sdk_path="$(xcrun -sdk iphoneos -show-sdk-path)" + cflags+=("-arch arm64") + cflags+=("-target arm64-apple-ios") + cflags+=("-Wno-unguarded-availability-new") + cflags+=("-miphoneos-version-min=$IPHONEOS_VERSION_MIN") + elif [ "$platform" == "ios-simulator" ]; then + ios_sdk_path="$(xcrun -sdk iphonesimulator -show-sdk-path)" + cflags+=("-arch $arch") + cflags+=("-mios-simulator-version-min=$IPHONEOS_VERSION_MIN") + fi + cflags+=("-iframeworkwithsysroot /System/Library/Frameworks") + cflags+=("-isysroot $ios_sdk_path/") + cflags+=("-F $ios_sdk_path/System/Library/Frameworks/") + cflags+=("-fembed-bitcode") +fi + +export CFLAGS="${cflags[@]}" +export LDFLAGS="${ldflags[@]}" +export DEPENDENCIES="${dependencies[@]}" +export VERSION="$version" +export LIB_DIRECTORY="$lib_directory" +export INCLUDE_DIRECTORY="$include_directory" + +rm -f "$output" + +if ! cat "$input" | mush > "$output"; then + exit $? +fi + +echo "Wrote pkgconfig file to '$output'" diff --git a/bin/install.sh b/bin/install.sh index feb89315be..850a03d056 100755 --- a/bin/install.sh +++ b/bin/install.sh @@ -234,7 +234,6 @@ function _build_cli { local test_headers=() if [[ -z "$ignore_header_mtimes" ]]; then test_headers+=("$(find "$src"/cli/*.hh 2>/dev/null)") - test_headers+=("$(ls "$src"/*.hh)") fi test_headers+=("$src"/../VERSION.txt) local newest_mtime=0 @@ -487,10 +486,10 @@ function _prebuild_ios_simulator_main () { function _prepare { echo "# preparing directories..." local arch="$(host_arch)" - rm -rf "$SOCKET_HOME"/{lib$d,src,bin,include,objects,api} + rm -rf "$SOCKET_HOME"/{lib$d,src,bin,include,objects,api,pkgconfig} rm -rf "$SOCKET_HOME"/{lib$d,objects}/"$arch-desktop" - mkdir -p "$SOCKET_HOME"/{lib$d,src,bin,include,objects,api} + mkdir -p "$SOCKET_HOME"/{lib$d,src,bin,include,objects,api,pkgconfig} mkdir -p "$SOCKET_HOME"/{lib$d,objects}/"$arch-desktop" if [[ "$host" = "Darwin" ]]; then @@ -572,6 +571,13 @@ function _install { exit 1 fi + if [ "$host" == "Linux" ]; then + echo "# copying pkgconfig to $SOCKET_HOME/pkgconfig" + rm -rf "$SOCKET_HOME/pkgconfig" + mkdir -p "$SOCKET_HOME/pkgconfig" + cp -rfp "$BUILD_DIR/$arch-$platform/pkgconfig"/* "$SOCKET_HOME/pkgconfig" + fi + if [ "$platform" == "desktop" ]; then echo "# copying js api to $SOCKET_HOME/api" mkdir -p "$SOCKET_HOME/api" @@ -897,7 +903,6 @@ function onsignal () { exit "$status" } -_check_compiler_features _prepare cd "$BUILD_DIR" || exit 1 @@ -966,6 +971,7 @@ if [[ "$host" == "Win32" ]]; then wait $_compile_libuv_pid fi +_check_compiler_features _build_runtime_library _build_cli & pids+=($!) diff --git a/bin/ldflags.sh b/bin/ldflags.sh index 915efe0d83..4d4eee4919 100755 --- a/bin/ldflags.sh +++ b/bin/ldflags.sh @@ -113,9 +113,7 @@ if [[ "$host" = "Darwin" ]]; then ldflags+=("-ldl") elif [[ "$host" = "Linux" ]]; then ldflags+=("-ldl") - if [ -z "$BUILDING_SSC_CLI" ]; then - ldflags+=($(pkg-config --libs gtk+-3.0 webkit2gtk-4.1)) - fi + ldflags+=($(pkg-config --libs gtk+-3.0 webkit2gtk-4.1)) elif [[ "$host" = "Win32" ]]; then if [[ -n "$DEBUG" ]]; then # https://learn.microsoft.com/en-us/cpp/c-runtime-library/crt-library-features?view=msvc-170 diff --git a/bin/mush.sh b/bin/mush.sh new file mode 100755 index 0000000000..e0c61742c8 --- /dev/null +++ b/bin/mush.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash + +## +# Mustache templates for bash (https://github.com/jwerle/mush) +# +# The MIT License (MIT) +# +# Copyright (c) 2013 Joseph Werle +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +## + +mush_version () { + echo "0.1.4" +} + +mush_usage () { + echo "usage: mush [-ehV] [-f <file>] [-o <file>]" + + if [ "$1" = "1" ]; then + echo + echo "examples:" + echo " $ cat file.ms | FOO=BAR mush" + echo " $ VALUE=123 mush -f file.ms -o file" + echo " $ echo \"Today's date is {{DATE}}\" | DATE=\`date +%D\` mush" + echo " $ cat ./template.ms | VAR=VALUE mush" + echo + echo "options:" + echo " -f, --file <file> file to parse" + echo " -o, --out <file> output file" + echo " -e, --escape escapes html html entities" + echo " -h, --help display this message" + echo " -V, --version output version" + fi +} + +mush () { + # shellcheck disable=SC2034 + local SELF="$0" + # shellcheck disable=SC2034 + local NULL=/dev/null + # shellcheck disable=SC2034 + local STDIN=0 + local STDOUT=1 + local STDERR=2 + local LEFT_DELIM="{{" + local RIGHT_DELIM="}}" + # shellcheck disable=SC2034 + local INDENT_LEVEL=" " + local ESCAPE=0 + local ENV="" + local out=">&$STDOUT" + + ENV="$(env)" + + ## parse opts + while true; do + arg="$1" + + if [ "" = "$1" ]; then + break; + fi + + if [ "${arg:0:1}" != "-" ]; then + shift + continue + fi + + case $arg in + -f|--file) + file="$2"; + shift 2; + ;; + -o|--out) + out="> $2"; + shift 2; + ;; + -e|--escape) + ESCAPE=1 + shift + ;; + -h|--help) + mush_usage 1 + exit 1 + ;; + -V|--version) + mush_version + exit 0 + ;; + *) + { + echo "unknown option \`$arg'" + } >&$STDERR + mush_usage + exit 1 + ;; + esac + done + + ## read each line + while IFS= read -r line; do + printf '%q\n' "${line}" | { + ## read each ENV variable + echo "$ENV" | { + while read -r var; do + ## split each ENV variable by '=' + ## and parse the line replacing + ## occurrence of the key with + ## guarded by the values of + ## `LEFT_DELIM' and `RIGHT_DELIM' + ## with the value of the variable + case "$var" in + (*"="*) + key=${var%%"="*} + val=${var#*"="*} + ;; + + (*) + key=$var + val= + ;; + esac + + line="${line//${LEFT_DELIM}$key${RIGHT_DELIM}/$val}" + done + + if [ "1" = "$ESCAPE" ]; then + line="${line//&/&}" + line="${line//\"/"}" + line="${line//\</<}" + line="${line//\>/>}" + fi + + ## output to stdout + echo "$line" | { + ## parse undefined variables + sed -e "s#${LEFT_DELIM}[A-Za-z]*${RIGHT_DELIM}##g" | \ + ## parse comments + sed -e "s#${LEFT_DELIM}\!.*${RIGHT_DELIM}##g" + }; + } + }; + done +} + + +if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then + export -f mush +else + if [ ! -t 0 ]; then + eval "mush $out" + elif [ -n "$file" ]; then + eval "cat $file | mush $out" + elif (( $# > 0 )); then + mush "$@" + else + mush_usage + exit 1 + fi + exit $? +fi diff --git a/bin/publish-npm-modules.sh b/bin/publish-npm-modules.sh index a1ad05c1cc..74f8bca420 100755 --- a/bin/publish-npm-modules.sh +++ b/bin/publish-npm-modules.sh @@ -174,6 +174,10 @@ if (( !only_top_level )); then cp -rf "$SOCKET_HOME/src"/* "$SOCKET_HOME/packages/$package/src" cp -rf "$SOCKET_HOME/include"/* "$SOCKET_HOME/packages/$package/include" + if test -d "$SOCKET_HOME/pkgconfig"; then + cp -rf "$SOCKET_HOME/pkgconfig" "$SOCKET_HOME/packages/$package/pkgconfig" + fi + # don't copy debug files, too large rm -rf $SOCKET_HOME/lib/*-android/objs-debug cp -rf $SOCKET_HOME/lib/*-android "$SOCKET_HOME/packages/$package/lib" diff --git a/socket-runtime.pc.in b/socket-runtime.pc.in new file mode 100644 index 0000000000..a608c699f2 --- /dev/null +++ b/socket-runtime.pc.in @@ -0,0 +1,7 @@ +Name: socket-runtime +Version: {{VERSION}} +Description: Build and package lean, fast, native desktop and mobile applications using the web technologies you already know. +URL: https://github.com/socketsupply/socket +Requires: {{DEPENDENCIES}} +Libs: -L{{LIB_DIRECTORY}} -lsocket-runtime -luv {{LDFLAGS}} +Cflags: -I{{INCLUDE_DIRECTORY}} {{CFLAGS}} From e4f288470e170b53dfb3515ef684b9f884fe500e Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Thu, 12 Oct 2023 12:18:37 -0400 Subject: [PATCH 190/256] refactor(core): fix windows build --- src/core/core.hh | 5 +++++ src/core/env.cc | 8 ++++---- src/extension/ipc.cc | 2 ++ src/window/win.cc | 46 ++++++++++++++++++++++---------------------- 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/core/core.hh b/src/core/core.hh index 738dbadccc..52d3c0225d 100644 --- a/src/core/core.hh +++ b/src/core/core.hh @@ -40,6 +40,11 @@ namespace SSC { uint64_t rand64 (); void msleep (uint64_t ms); +#if defined(_WIN32) + String FormatError (DWORD error, String source); +#endif + + // forward class Core; diff --git a/src/core/env.cc b/src/core/env.cc index 159f760d00..750c9897d4 100644 --- a/src/core/env.cc +++ b/src/core/env.cc @@ -88,7 +88,7 @@ namespace SSC { void Env::set (const String& name, const String& value) { #if defined(_WIN32) - return _putenv((name + "=" + value).c_str()); + _putenv((name + "=" + value).c_str()); #else setenv(name.c_str(), value.c_str(), 1); #endif @@ -96,10 +96,10 @@ namespace SSC { void Env::set (const char* name) { #if defined(_WIN32) - return _putenv(name); - #endif - + _putenv(name); + #else auto parts = split(String(name), '='); set(parts[0], parts[1]); + #endif } } diff --git a/src/extension/ipc.cc b/src/extension/ipc.cc index 042173baa3..e66dc160db 100644 --- a/src/extension/ipc.cc +++ b/src/extension/ipc.cc @@ -655,6 +655,7 @@ void sapi_ipc_result_set_header ( if (result && name && value) { result->headers.set(name, value); + #if !defined(_WIN32) if (strcasecmp(name, "content-type") == 0 && strcasecmp(value, "text/event-stream") == 0) { result->post = SSC::Post(); @@ -672,6 +673,7 @@ void sapi_ipc_result_set_header ( return false; }); } + #endif } } diff --git a/src/window/win.cc b/src/window/win.cc index d99ececc01..183a38578e 100644 --- a/src/window/win.cc +++ b/src/window/win.cc @@ -20,7 +20,7 @@ using namespace Microsoft::WRL; namespace SSC { static inline void alert (const SSC::WString &ws) { - MessageBoxA(nullptr, SSC::WStringToString(ws).c_str(), _TEXT("Alert"), MB_OK | MB_ICONSTOP); + MessageBoxA(nullptr, SSC::convertWStringToString(ws).c_str(), _TEXT("Alert"), MB_OK | MB_ICONSTOP); } static inline void alert (const SSC::String &s) { @@ -654,8 +654,8 @@ namespace SSC { wchar_t modulefile[MAX_PATH]; GetModuleFileNameW(NULL, modulefile, MAX_PATH); auto file = (fs::path { modulefile }).filename(); - auto filename = SSC::StringToWString(file.string()); - auto path = SSC::StringToWString(getEnv("APPDATA")); + auto filename = SSC::convertStringToWString(file.string()); + auto path = SSC::convertStringToWString(Env::get("APPDATA")); this->modulePath = fs::path(modulefile); auto options = Microsoft::WRL::Make<CoreWebView2EnvironmentOptions>(); @@ -744,7 +744,7 @@ namespace SSC { wchar_t* buf = new wchar_t[l+1]; GetWindowTextW(hWnd, buf, l+1); - if (SSC::WStringToString(buf).find("Chrome") != SSC::String::npos) { + if (SSC::convertWStringToString(buf).find("Chrome") != SSC::String::npos) { RevokeDragDrop(hWnd); Window* w = reinterpret_cast<Window*>(GetWindowLongPtr((HWND)window, GWLP_USERDATA)); w->drop->childWindow = hWnd; @@ -763,7 +763,7 @@ namespace SSC { [&](ICoreWebView2*, ICoreWebView2NavigationStartingEventArgs *e) { PWSTR uri; e->get_Uri(&uri); - SSC::String url(SSC::WStringToString(uri)); + SSC::String url(SSC::convertWStringToString(uri)); if (url.find("socket:") != 0 && url.find("file://") != 0 && url.find("http://localhost") != 0) { e->put_Cancel(true); @@ -803,11 +803,11 @@ namespace SSC { args->get_Request(&req); req->get_Uri(&req_uri); - uri = WStringToString(req_uri); + uri = convertWStringToString(req_uri); CoTaskMemFree(req_uri); req->get_Method(&req_method); - method = WStringToString(req_method); + method = convertWStringToString(req_method); CoTaskMemFree(req_method); bool ipc_scheme = false; @@ -912,7 +912,7 @@ namespace SSC { bytes, 200, L"OK", - StringToWString(headers).c_str(), + convertStringToWString(headers).c_str(), &res ); args->put_Response(res); @@ -989,7 +989,7 @@ namespace SSC { nullptr, 200, L"OK", - StringToWString(headers).c_str(), + convertStringToWString(headers).c_str(), &res ); args->put_Response(res); @@ -1005,7 +1005,7 @@ namespace SSC { bytes, 200, L"OK", - StringToWString(headers).c_str(), + convertStringToWString(headers).c_str(), &res ); args->put_Response(res); @@ -1025,8 +1025,8 @@ namespace SSC { 301, L"Moved Permanently", WString( - StringToWString("Location: ") + StringToWString(uri) + L"\n" + - StringToWString("Content-Location: ") + StringToWString(uri) + L"\n" + convertStringToWString("Location: ") + convertStringToWString(uri) + L"\n" + + convertStringToWString("Content-Location: ") + convertStringToWString(uri) + L"\n" ).c_str(), &res ); @@ -1080,11 +1080,11 @@ namespace SSC { } else if (path.ends_with(".ogv")) { mimeType = (wchar_t*) L"video/ogg"; } else { - FindMimeFromData(0, StringToWString(path).c_str(), 0, 0, 0, 0, &mimeType, 0); + FindMimeFromData(0, convertStringToWString(path).c_str(), 0, 0, 0, 0, &mimeType, 0); } headers = "Content-Type: "; - headers += WStringToString(mimeType) + "\n"; + headers += convertWStringToString(mimeType) + "\n"; headers += "Connection: keep-alive\n"; headers += "Access-Control-Allow-Headers: *\n"; headers += "Access-Control-Allow-Origin: *\n"; @@ -1097,7 +1097,7 @@ namespace SSC { stream, 200, L"OK", - StringToWString(headers).c_str(), + convertStringToWString(headers).c_str(), &res ); } else { @@ -1155,7 +1155,7 @@ namespace SSC { webview->AddScriptToExecuteOnDocumentCreated( // Note that this may not do anything as preload goes out of scope before event fires // Consider using w->preloadJavascript, but apps work without this - SSC::StringToWString(preload).c_str(), + SSC::convertStringToWString(preload).c_str(), Microsoft::WRL::Callback<ICoreWebView2AddScriptToExecuteOnDocumentCreatedCompletedHandler>( [&](HRESULT error, PCWSTR id) -> HRESULT { return S_OK; @@ -1172,7 +1172,7 @@ namespace SSC { SSC::WString message_w(messageRaw); CoTaskMemFree(messageRaw); if (onMessage != nullptr) { - SSC::String message = SSC::WStringToString(message_w); + SSC::String message = SSC::convertWStringToString(message_w); auto msg = IPC::Message{message}; Window* w = reinterpret_cast<Window*>(GetWindowLongPtr((HWND)window, GWLP_USERDATA)); ICoreWebView2_2* webview2 = nullptr; @@ -1202,7 +1202,7 @@ namespace SSC { cshr = webView18->PostSharedBufferToScript( sharedBuffer, COREWEBVIEW2_SHARED_BUFFER_ACCESS_READ_WRITE, - StringToWString(additionalData).c_str() + convertStringToWString(additionalData).c_str() ); IPC::MessageBuffer msg_buf(sharedBuffer, size); // TODO(trevnorris): This will leak memory if the buffer is created and @@ -1436,7 +1436,7 @@ namespace SSC { } this->webview->ExecuteScript( - SSC::StringToWString(s).c_str(), + SSC::convertStringToWString(s).c_str(), nullptr ); }); @@ -1468,7 +1468,7 @@ namespace SSC { &token ); - webview->Navigate(SSC::StringToWString(value).c_str()); + webview->Navigate(SSC::convertStringToWString(value).c_str()); }); } @@ -1476,7 +1476,7 @@ namespace SSC { int len = GetWindowTextLength(window) + 1; LPTSTR title = new TCHAR[len]; GetWindowText(window, title, len); - String title_s = WStringToString(title); + String title_s = convertWStringToString(title); delete[] title; return title_s; } @@ -1908,7 +1908,7 @@ namespace SSC { return; } - result_paths.push_back(SSC::WStringToString(SSC::WString(buf))); + result_paths.push_back(SSC::convertWStringToString(SSC::WString(buf))); single_result->Release(); CoTaskMemFree(buf); @@ -1941,7 +1941,7 @@ namespace SSC { return; } - result_paths.push_back(SSC::WStringToString(SSC::WString(buf))); + result_paths.push_back(SSC::convertWStringToString(SSC::WString(buf))); path->Release(); CoTaskMemFree(buf); } From 0ef725251b3d883599680bd728d3114bf8a57b64 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Thu, 12 Oct 2023 14:23:41 -0400 Subject: [PATCH 191/256] refactor(src/core/core.hh): guard 'file_system_watcher' from android --- src/core/core.hh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/core.hh b/src/core/core.hh index 52d3c0225d..f0cbb0dd03 100644 --- a/src/core/core.hh +++ b/src/core/core.hh @@ -5,7 +5,6 @@ #include "config.hh" #include "debug.hh" #include "env.hh" -#include "file_system_watcher.hh" #include "ini.hh" #include "io.hh" #include "json.hh" @@ -15,6 +14,10 @@ #include "types.hh" #include "version.hh" +#if !defined(__ANDROID__) +#include "file_system_watcher.hh" +#endif + #if defined(__APPLE__) @interface SSCBluetoothController : NSObject< CBCentralManagerDelegate, From f79cea965e20db79a10e2cac64c0caa5d35be868 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Thu, 12 Oct 2023 14:24:35 -0400 Subject: [PATCH 192/256] chore(src/cli/cli.cc): fix missing generated android files --- src/cli/cli.cc | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 5bee0413d4..009d9fbe92 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -2995,8 +2995,7 @@ int main (const int argc, const char* argv[]) { if (debugEnv || verboseEnv) log("sdkmanager --version 2>&1 >/dev/null"); sdkmanager << androidHome; - if (Env::get("ANDROID_SDK_MANAGER").size() > 0) - { + if (Env::get("ANDROID_SDK_MANAGER").size() > 0) { sdkmanager << "/" << Env::get("ANDROID_SDK_MANAGER"); } else if (std::system(" sdkmanager --version 2>&1 >/dev/null") != 0) { if (!platform.win) { @@ -3058,23 +3057,30 @@ int main (const int argc, const char* argv[]) { exit(1); } - // Core + // user entry fs::copy(trim(prefixFile("src/init.cc")), jni, fs::copy_options::overwrite_existing); + // android runtime fs::copy(trim(prefixFile("src/android/bridge.cc")), jni / "android", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/android/runtime.cc")), jni / "android", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/android/string_wrap.cc")), jni / "android", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/android/window.cc")), jni / "android", fs::copy_options::overwrite_existing); - fs::copy(trim(prefixFile("src/core/config.hh")), jni, fs::copy_options::overwrite_existing); + // core + fs::copy(trim(prefixFile("src/core/codec.hh")), jni / "core", fs::copy_options::overwrite_existing); + fs::copy(trim(prefixFile("src/core/config.hh")), jni / "core", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/core/core.hh")), jni / "core", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/core/debug.hh")), jni / "core", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/core/env.hh")), jni / "core", fs::copy_options::overwrite_existing); + fs::copy(trim(prefixFile("src/core/ini.hh")), jni / "core", fs::copy_options::overwrite_existing); + fs::copy(trim(prefixFile("src/core/io.hh")), jni / "core", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/core/json.hh")), jni / "core", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/core/platform.hh")), jni / "core", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/core/preload.hh")), jni / "core", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/core/string.hh")), jni / "core", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/core/types.hh")), jni / "core", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/core/version.hh")), jni / "core", fs::copy_options::overwrite_existing); + // ipc fs::copy(trim(prefixFile("src/ipc/ipc.hh")), jni / "ipc", fs::copy_options::overwrite_existing); + // window fs::copy(trim(prefixFile("src/window/options.hh")), jni / "window", fs::copy_options::overwrite_existing); fs::copy(trim(prefixFile("src/window/window.hh")), jni / "window", fs::copy_options::overwrite_existing); From d8124d6d7753d2b225480dab5470b90b791d3a38 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Thu, 12 Oct 2023 14:26:32 -0400 Subject: [PATCH 193/256] fix(android): fix typos --- src/android/main.kt | 4 ++-- src/android/runtime.cc | 1 - src/android/runtime.kt | 3 +++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/android/main.kt b/src/android/main.kt index 71bee625c3..797e7bbf5a 100644 --- a/src/android/main.kt +++ b/src/android/main.kt @@ -107,7 +107,7 @@ open class MainActivity : WebViewActivity() { this.runtime.setIsEmulator( ( android.os.Build.BRAND.startsWith("generic") && - android.ios.Build.DEVICE.startsWith("generic") + android.os.Build.DEVICE.startsWith("generic") ) || android.os.Build.FINGERPRINT.startsWith("generic") || android.os.Build.FINGERPRINT.startsWith("unknown") || @@ -124,7 +124,7 @@ open class MainActivity : WebViewActivity() { android.os.Build.PRODUCT.contains("sdk_gphone64_arm64") || android.os.Build.PRODUCT.contains("vbox86p") || android.os.Build.PRODUCT.contains("emulator") || - android.os.Build.PRODUCT.contains("simulator") || + android.os.Build.PRODUCT.contains("simulator") ) this.window = Window(this.runtime, this) diff --git a/src/android/runtime.cc b/src/android/runtime.cc index ac26938096..c3f348adc5 100644 --- a/src/android/runtime.cc +++ b/src/android/runtime.cc @@ -184,7 +184,6 @@ extern "C" { jboolean value ) { auto runtime = Runtime::from(env, self); - auto name = StringWrap(env, permission).str(); if (runtime == nullptr) { Throw(env, RuntimeNotInitializedException); diff --git a/src/android/runtime.kt b/src/android/runtime.kt index 4d79a47ff3..b2bed3ea32 100644 --- a/src/android/runtime.kt +++ b/src/android/runtime.kt @@ -81,4 +81,7 @@ open class Runtime ( @Throws(java.lang.Exception::class) external fun isPermissionAllowed (permission: String): Boolean; + + @Throws(java.lang.Exception::class) + external fun setIsEmulator (value: Boolean): Boolean; } From 405326030e02d1d3dee79dc3b4c87506ba68fa95 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Thu, 12 Oct 2023 14:45:39 -0400 Subject: [PATCH 194/256] fix(test): fix extension build script phase --- test/scripts/{init.sh => init-sqlite3-build.sh} | 0 test/socket.ini | 1 - test/src/extensions/sqlite3/socket.ini | 3 +++ 3 files changed, 3 insertions(+), 1 deletion(-) rename test/scripts/{init.sh => init-sqlite3-build.sh} (100%) diff --git a/test/scripts/init.sh b/test/scripts/init-sqlite3-build.sh similarity index 100% rename from test/scripts/init.sh rename to test/scripts/init-sqlite3-build.sh diff --git a/test/socket.ini b/test/socket.ini index bf270ae4d0..39ad340774 100644 --- a/test/socket.ini +++ b/test/socket.ini @@ -3,7 +3,6 @@ name = "socket-runtime-javascript-tests" copy = src copy_map = src/mapping.ini output = build -script = sh scripts/shell.sh scripts/init.sh ; Compiler Settings flags = "-O3 -g" diff --git a/test/src/extensions/sqlite3/socket.ini b/test/src/extensions/sqlite3/socket.ini index e5421ebef3..1fc7e1dfbd 100644 --- a/test/src/extensions/sqlite3/socket.ini +++ b/test/src/extensions/sqlite3/socket.ini @@ -7,6 +7,9 @@ sources[] = ./extension.cc sources[] = ../../../build/sqlite3/sqlite3.h sources[] = ../../../build/sqlite3/sqlite3.c +[extension.build] +script = sh ../../../scripts/shell.sh scripts/init-sqlite3-build.sh + [extension.compiler] flags[] = -DSQLITE_ENABLE_COLUMN_METADATA flags[] = -DDSQLITE_ENABLE_SESSION From 9610fbe135adefbf1d63ee703d79d8bf60f605ff Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Thu, 12 Oct 2023 15:50:44 -0400 Subject: [PATCH 195/256] fix(src/cli/cli.cc): make unresolvd paths in desktop extension less brittle --- src/cli/cli.cc | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 009d9fbe92..969511d613 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -4892,6 +4892,7 @@ int main (const int argc, const char* argv[]) { source = settings["build_extensions_" + extension]; if (source.size() > 0) { if (fs::is_directory(source)) { + settings["build_extensions_" + extension + "_path"] = (targetPath / source).string(); settings["build_extensions_" + extension] = ""; } } @@ -4900,7 +4901,7 @@ int main (const int argc, const char* argv[]) { if (source.size() > 0) { Path target; if (fs::exists(source)) { - target = source; + target = targetPath / source; } else if (source.ends_with(".git")) { auto path = Path { source }; target = paths.platformSpecificOutputPath / "extensions" / replace(path.filename().string(), ".git", ""); @@ -4927,7 +4928,11 @@ int main (const int argc, const char* argv[]) { const auto sources = parseStringList(entry.second, ' '); Vector<String> canonical; for (const auto& source : sources) { - canonical.push_back(fs::canonical(target / source).string()); + try { + canonical.push_back(fs::canonical(target / source).string()); + } catch (const std::filesystem::filesystem_error& e) { + canonical.push_back((target / source).string()); + } } settings["build_extensions_" + extension] = join(canonical, " "); @@ -4979,8 +4984,8 @@ int main (const int argc, const char* argv[]) { ); } catch (const std::filesystem::filesystem_error& e) { if (e.code() == std::errc::no_such_file_or_directory) { - log("ERROR: path not found: " + absolutePath.string()); - exit(1); + log("WARNING: path not found: " + absolutePath.string()); + break; } else { throw e; } From 807199630797b98d8cff30bca5645f28c764df24 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Thu, 12 Oct 2023 16:49:18 -0400 Subject: [PATCH 196/256] fix(cli): track built extensions for android --- src/cli/cli.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 969511d613..5dd82353ab 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -3329,6 +3329,7 @@ int main (const int argc, const char* argv[]) { ); } + Vector<String> seenExtensions; for (const auto& tuple : settings) { auto& key = tuple.first; if (tuple.second.size() == 0) continue; @@ -3353,6 +3354,11 @@ int main (const int argc, const char* argv[]) { extension = split(extension, '_')[0]; + if (std::find(seenExtensions.begin(), seenExtensions.end(), extension) != seenExtensions.end()) { + continue; + } + seenExtensions.push_back(extension); + auto source = settings["build_extensions_" + extension + "_source"]; auto oldCwd = fs::current_path(); fs::current_path(targetPath); From ee90de2549c531b37e45c2fd4d58b22c903b1e09 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Fri, 13 Oct 2023 12:03:25 -0400 Subject: [PATCH 197/256] fix: remove outdated code in `getResolveMenuSelectionJavaScript` --- src/core/javascript.cc | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/core/javascript.cc b/src/core/javascript.cc index 4ebf62cff1..021a4b509b 100644 --- a/src/core/javascript.cc +++ b/src/core/javascript.cc @@ -76,12 +76,6 @@ namespace SSC { " state: '0' \n" "}; \n" " \n" - " if (" + seq + " > 0 && globalThis._ipc['R" + seq + "']) { \n" - " globalThis._ipc['R" + seq + "'].resolve(detail); \n" - " delete globalThis._ipc['R" + seq + "']; \n" - " return; \n" - " } \n" - " \n" "const event = new globalThis.CustomEvent('menuItemSelected', { \n" " detail \n" "}); \n" From cd09b93b3802c7e2c3f2072cc24cf25dfb621bba Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sat, 14 Oct 2023 12:22:05 -0400 Subject: [PATCH 198/256] fix(apple): skip `didReceiveData` call if both event name and data are empty --- src/ipc/bridge.cc | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/ipc/bridge.cc b/src/ipc/bridge.cc index 8b28cca2ab..d909cd4cee 100644 --- a/src/ipc/bridge.cc +++ b/src/ipc/bridge.cc @@ -2618,15 +2618,17 @@ static void registerSchemeHandler (Router *router) { bool finished) { auto event_name = [NSString stringWithUTF8String:name]; auto event_data = [NSString stringWithUTF8String:data]; - auto event = - event_name.length > 0 && event_data.length > 0 - ? [NSString stringWithFormat:@"event: %@\ndata: %@\n\n", - event_name, event_data] - : event_data.length > 0 - ? [NSString stringWithFormat:@"data: %@\n\n", event_data] - : [NSString stringWithFormat:@"event: %@\n\n", event_name]; - - [task didReceiveData:[event dataUsingEncoding:NSUTF8StringEncoding]]; + if (event_name.length > 0 || event_data.length > 0) { + auto event = + event_name.length > 0 && event_data.length > 0 + ? [NSString stringWithFormat:@"event: %@\ndata: %@\n\n", + event_name, event_data] + : event_data.length > 0 + ? [NSString stringWithFormat:@"data: %@\n\n", event_data] + : [NSString stringWithFormat:@"event: %@\n\n", event_name]; + + [task didReceiveData:[event dataUsingEncoding:NSUTF8StringEncoding]]; + } if (finished) { [task didFinish]; } From c256a8fa1702cfe30e07464cfcffa27280ac85e3 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sat, 14 Oct 2023 12:13:57 -0400 Subject: [PATCH 199/256] fix(extension): change `retained` boolean to `retain_count` integer So multiple retain/release call pairs can co-exist. --- src/extension/context.cc | 9 +++++---- src/extension/extension.cc | 15 +++++++++++---- src/extension/extension.hh | 4 ++-- src/extension/ipc.cc | 3 +-- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/extension/context.cc b/src/extension/context.cc index 7190c7da9b..1a7a23d3b0 100644 --- a/src/extension/context.cc +++ b/src/extension/context.cc @@ -15,7 +15,7 @@ sapi_context_t* sapi_context_create ( : parent->memory.alloc<sapi_context_t>(parent, parent); if (retained || parent == nullptr) { - context->retained = true; + context->retain(); } return context; @@ -62,7 +62,7 @@ void sapi_context_retain (sapi_context_t* ctx) { bool sapi_context_retained (const sapi_context_t* ctx) { if (ctx == nullptr) return false; - return ctx->retained; + return ctx->retain_count > 0; } void sapi_context_release (sapi_context_t* ctx) { @@ -71,8 +71,9 @@ void sapi_context_release (sapi_context_t* ctx) { sapi_debug(ctx, "'context_release' is not allowed."); return; } - ctx->release(); - delete ctx; + if (ctx->release()) { + delete ctx; + } } uv_loop_t* sapi_context_get_loop (const sapi_context_t* ctx) { diff --git a/src/extension/extension.cc b/src/extension/extension.cc index 03df1c9abf..23761b5faf 100644 --- a/src/extension/extension.cc +++ b/src/extension/extension.cc @@ -139,12 +139,19 @@ namespace SSC { } void Extension::Context::retain () { - this->retained = true; + this->retain_count++; } - void Extension::Context::release () { - this->retained = false; - this->memory.release(); + bool Extension::Context::release () { + if (this->retain_count == 0) { + debug("WARN - Double release of SSC extension context"); + return false; + } + if (--this->retain_count == 0) { + this->memory.release(); + return true; + } + return false; } Extension::Context* Extension::getContext (const String& name) { diff --git a/src/extension/extension.hh b/src/extension/extension.hh index 14336033b2..00b0b7b25e 100644 --- a/src/extension/extension.hh +++ b/src/extension/extension.hh @@ -84,7 +84,7 @@ namespace SSC { Memory memory; State state = State::None; Error error; - std::atomic<bool> retained = false; + std::atomic<unsigned int> retain_count = 0; PolicyMap policies; Map config; @@ -96,7 +96,7 @@ namespace SSC { Context (const Context& context, IPC::Router* router); void retain (); - void release (); + bool release (); void setPolicy (const String& name, bool allowed); const Policy& getPolicy (const String& name) const; diff --git a/src/extension/ipc.cc b/src/extension/ipc.cc index e66dc160db..004bf2c869 100644 --- a/src/extension/ipc.cc +++ b/src/extension/ipc.cc @@ -137,8 +137,7 @@ bool sapi_ipc_reply (const sapi_ipc_result_t* result) { } // if retained, then then caller must eventually call `sapi_context_release()` - if (!context->retained) { - context->release(); + if (context->release()) { delete context; } From fb4cc1dd8b29563c176a5a5af7811f2b3c77181d Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sat, 14 Oct 2023 12:14:30 -0400 Subject: [PATCH 200/256] fix(extension): add debug log for incorrect use of send_event/send_chunk --- src/extension/ipc.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/extension/ipc.cc b/src/extension/ipc.cc index 004bf2c869..851e04f433 100644 --- a/src/extension/ipc.cc +++ b/src/extension/ipc.cc @@ -160,6 +160,9 @@ bool sapi_ipc_send_chunk ( } auto send_chunk_ptr = result->post.chunk_stream; if (send_chunk_ptr == nullptr) { + debug( + "Cannot use 'sapi_ipc_send_chunk' before setting the \"Transfer-Encoding\"" + " header to \"chunked\""); return false; } bool success = (*send_chunk_ptr)(chunk, chunk_size, finished); @@ -185,6 +188,9 @@ bool sapi_ipc_send_event ( } auto send_event_ptr = result->post.event_stream; if (send_event_ptr == nullptr) { + debug( + "Cannot use 'sapi_ipc_send_event' before setting the \"Content-Type\"" + " header to \"text/event-stream\""); return false; } bool success = (*send_event_ptr)(name, data, finished); From df68857e2abb90363963447773d5d2e5cd698df5 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sat, 14 Oct 2023 12:15:30 -0400 Subject: [PATCH 201/256] fix(extension): automatically retain the context when certain headers are set This balances out the release calls made when send_event/send_chunk are called with a finished value of true. --- src/extension/ipc.cc | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/extension/ipc.cc b/src/extension/ipc.cc index 851e04f433..bdbd300adb 100644 --- a/src/extension/ipc.cc +++ b/src/extension/ipc.cc @@ -167,7 +167,10 @@ bool sapi_ipc_send_chunk ( } bool success = (*send_chunk_ptr)(chunk, chunk_size, finished); if (finished) { - sapi_context_release(result->context); + auto context = result->context; + if (context->release()) { + delete context; + } } return success; } @@ -195,7 +198,10 @@ bool sapi_ipc_send_event ( } bool success = (*send_event_ptr)(name, data, finished); if (finished) { - sapi_context_release(result->context); + auto context = result->context; + if (context->release()) { + delete context; + } } return success; } @@ -663,6 +669,7 @@ void sapi_ipc_result_set_header ( #if !defined(_WIN32) if (strcasecmp(name, "content-type") == 0 && strcasecmp(value, "text/event-stream") == 0) { + result->context->retain(); result->post = SSC::Post(); result->post.event_stream = std::make_shared<std::function<bool(const char*, const char*, bool)>>( @@ -671,6 +678,7 @@ void sapi_ipc_result_set_header ( }); } else if (strcasecmp(name, "transfer-encoding") == 0 && strcasecmp(value, "chunked") == 0) { + result->context->retain(); result->post = SSC::Post(); result->post.chunk_stream = std::make_shared<std::function<bool(const char*, size_t, bool)>>( From 6f25e537efa76776823cb63048776416d55207a9 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Sat, 14 Oct 2023 16:27:27 -0400 Subject: [PATCH 202/256] fix: allocate the route context for each message --- src/extension/ipc.cc | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/extension/ipc.cc b/src/extension/ipc.cc index bdbd300adb..60cca4dd62 100644 --- a/src/extension/ipc.cc +++ b/src/extension/ipc.cc @@ -19,20 +19,18 @@ bool sapi_ipc_router_map ( return false; } - auto router = ctx->router; - auto context = sapi_context_create(ctx, true); - - if (context == nullptr) { - return false; - } - - context->data = data; - ctx->router->map(name, [context, data, callback]( + ctx->router->map(name, [ctx, data, callback]( auto& message, auto router, auto reply ) mutable { auto msg = SSC::IPC::Message(message); + auto context = sapi_context_create(ctx, true); + if (context == nullptr) { + return; + } + + context->data = data; context->internal = new SSC::IPC::Router::ReplyCallback(reply); callback( context, From 77c28339b128e8589c25a70922cf4401b3e642ac Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Mon, 16 Oct 2023 18:12:11 +0200 Subject: [PATCH 203/256] fix(src/cli/cli.cc): fix arguments parsing --- src/cli/cli.cc | 136 +++++++++++++++++++++++-------------------------- 1 file changed, 65 insertions(+), 71 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 5dd82353ab..ba634b78e6 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -1692,25 +1692,6 @@ void run (const String& targetPlatform, Map& settings, const Paths& paths, const exit(1); } -void handleOption( - Map& optionsWithValue, - std::unordered_set<String>& optionsWithoutValue, - const String& key, - const String& value, - const String& subcommand -) { - if (optionsWithValue.count(key) > 0 || optionsWithoutValue.find(key) != optionsWithoutValue.end()) { - std::cerr << "ERROR: Option '" << key << "' is used more than once." << std::endl; - printHelp(subcommand); - exit(1); - } - if (!value.empty()) { - optionsWithValue[key] = value; - } else { - optionsWithoutValue.insert(key); - } -} - struct Option { std::vector<String> aliases; bool isOptional; @@ -1718,38 +1699,6 @@ struct Option { }; using Options = std::vector<Option>; -String validateOption ( - const String& key, - const String& value, - const Options& availableOptions, - const String& subcommand -) { - bool recognized = false; - bool shouldHaveValue = false; - String result; - for (const auto& option : availableOptions) { - auto aliases = option.aliases; - shouldHaveValue = option.shouldHaveValue; - auto it = std::find(aliases.begin(), aliases.end(), key); - if (it != aliases.end()) { - recognized = true; - result = aliases[0]; - break; - } - } - if (!recognized) { - std::cerr << "ERROR: unrecognized option '" << key << "'" << std::endl; - printHelp(subcommand); - exit(1); - } - if (shouldHaveValue && value.empty()) { - std::cerr << "ERROR: option '" << key << "' requires a value" << std::endl; - printHelp(subcommand); - exit(1); - } - return result; -} - struct optionsAndEnv { Map optionsWithValue; std::unordered_set<String> optionsWithoutValue; @@ -1771,39 +1720,89 @@ optionsAndEnv parseCommandLineOptions ( size_t equalPos = arg.find('='); String key; String value; + bool shouldHaveValue = false; + bool isOptional = false; if (arg == "-h" || arg == "--help") { printHelp(subcommand); exit(0); } - if (equal(key, "--verbose")) { + if (equal(arg, "--verbose")) { flagVerboseMode = true; Env::set("SSC_VERBOSE", "1"); continue; } - if (equal(key, "--debug")) { + if (equal(arg, "--debug")) { Env::set("SSC_DEBUG", "1"); continue; } + bool hasEqualSignDelimiter = equalPos != String::npos; // Option in the form "--key=value" or "-k=value" - if (equalPos != String::npos) { + if (hasEqualSignDelimiter) { key = arg.substr(0, equalPos); value = arg.substr(equalPos + 1); } else { key = arg; - // Option in the form "--key value" or "-k value" - if (i + 1 < options.size() && options[i + 1][0] != '-') { - value = options[++i]; - } else { - // Option in the form "--key" or "-k" - value = ""; - if (i + 1 < options.size() && options[i + 1][0] != '-') { - targetPath = fs::absolute(options[i + 1]).lexically_normal(); + } + + // path + if (key.size() && !key.starts_with("-")) { + targetPath = fs::absolute(key).lexically_normal(); + value = ""; + key = ""; + continue; + } + + if (optionsWithValue.count(key) > 0 || optionsWithoutValue.find(key) != optionsWithoutValue.end()) { + std::cerr << "ERROR: Option '" << key << "' is used more than once." << std::endl; + printHelp(subcommand); + exit(1); + } + + // find option + Option recognizedOption; + bool found = false; + for (const auto option : availableOptions) { + for (const auto alias : option.aliases) { + if (alias == key) { + recognizedOption = option; + found = true; + key = recognizedOption.aliases[0]; + if (!recognizedOption.shouldHaveValue && value.size() > 0) { + std::cerr << "ERROR: option '" << key << "' does not require a value" << std::endl; + printHelp(subcommand); + exit(1); + } + if (!hasEqualSignDelimiter && recognizedOption.shouldHaveValue) { + // Option in the form "--key value" or "-k value" + if (i + 1 < options.size() && options[i + 1][0] != '-') { + value = options[++i]; + // Option in the form "--key" or "-k" + } else { + value = ""; + if (i + 1 < options.size() && options[i + 1][0] != '-') { + targetPath = fs::absolute(options[i + 1]).lexically_normal(); + } + } + } + if (!recognizedOption.isOptional && value.empty()) { + std::cerr << "ERROR: option '" << key << "' requires a value" << std::endl; + printHelp(subcommand); + exit(1); + } + break; } } + if (found) break; + } + + if (!found) { + std::cerr << "ERROR: unrecognized option '" << key << "'" << std::endl; + printHelp(subcommand); + exit(1); } if (value.size() == 0) { @@ -1820,15 +1819,10 @@ optionsAndEnv parseCommandLineOptions ( continue; } - if (key.size() && !key.starts_with("-")) { - targetPath = fs::absolute(key).lexically_normal(); - value = ""; - key = ""; - } - - if (key.size() > 0) { - auto option = validateOption(key, value, availableOptions, subcommand); - handleOption(optionsWithValue, optionsWithoutValue, option, value, subcommand); + if (!value.empty()) { + optionsWithValue[key] = value; + } else { + optionsWithoutValue.insert(key); } } From fdf4597c25717ec29b7fb295ca35495e0cfac985 Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Mon, 16 Oct 2023 18:20:07 +0200 Subject: [PATCH 204/256] fix(src/cli/cli.cc): remove unused variables --- src/cli/cli.cc | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index ba634b78e6..ba37193aae 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -1720,8 +1720,6 @@ optionsAndEnv parseCommandLineOptions ( size_t equalPos = arg.find('='); String key; String value; - bool shouldHaveValue = false; - bool isOptional = false; if (arg == "-h" || arg == "--help") { printHelp(subcommand); From 224dff5abd6ed2ccea60970b300423c736ad2aa7 Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Mon, 16 Oct 2023 18:23:08 +0200 Subject: [PATCH 205/256] fix(src/cli/cli.cc): fix check for duplicate options --- src/cli/cli.cc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index ba37193aae..4ac77ccab2 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -1754,12 +1754,6 @@ optionsAndEnv parseCommandLineOptions ( continue; } - if (optionsWithValue.count(key) > 0 || optionsWithoutValue.find(key) != optionsWithoutValue.end()) { - std::cerr << "ERROR: Option '" << key << "' is used more than once." << std::endl; - printHelp(subcommand); - exit(1); - } - // find option Option recognizedOption; bool found = false; @@ -1803,6 +1797,12 @@ optionsAndEnv parseCommandLineOptions ( exit(1); } + if (optionsWithValue.count(key) > 0 || optionsWithoutValue.find(key) != optionsWithoutValue.end()) { + std::cerr << "ERROR: Option '" << key << "' is used more than once." << std::endl; + printHelp(subcommand); + exit(1); + } + if (value.size() == 0) { value = rc[subcommand + "_" + key]; } From 4252d2fe493094f15a09eea5aad032fe5b383422 Mon Sep 17 00:00:00 2001 From: Bret Comnes <166301+bcomnes@users.noreply.github.com> Date: Mon, 16 Oct 2023 11:42:36 -0700 Subject: [PATCH 206/256] Implement 'resolveURLPathForWebView()' function (#663) --- src/android/main.kt | 5 - src/android/webview.kt | 93 +++++++++++-- src/ipc/bridge.cc | 129 ++++++++++++++---- src/ipc/ipc.hh | 7 + src/window/win.cc | 28 ++-- test/src/index.js | 1 + test/src/router-resolution.js | 86 ++++++++++++ .../router-resolution/a-conflict-index.html | 1 + .../a-conflict-index/index.html | 1 + .../an-index-file/a-html-file.html | 1 + .../an-index-file/index.html | 1 + test/src/router-resolution/another-file.html | 1 + test/src/router-resolution/index.html | 1 + test/src/router-resolution/pages/index.html | 1 + 14 files changed, 303 insertions(+), 53 deletions(-) create mode 100644 test/src/router-resolution.js create mode 100644 test/src/router-resolution/a-conflict-index.html create mode 100644 test/src/router-resolution/a-conflict-index/index.html create mode 100644 test/src/router-resolution/an-index-file/a-html-file.html create mode 100644 test/src/router-resolution/an-index-file/index.html create mode 100644 test/src/router-resolution/another-file.html create mode 100644 test/src/router-resolution/index.html create mode 100644 test/src/router-resolution/pages/index.html diff --git a/src/android/main.kt b/src/android/main.kt index 797e7bbf5a..6b36dc7eee 100644 --- a/src/android/main.kt +++ b/src/android/main.kt @@ -46,11 +46,6 @@ open class MainActivity : WebViewActivity() { } } - fun getRootDirectory (): String { - return getExternalFilesDir(null)?.absolutePath - ?: "/sdcard/Android/data/__BUNDLE_IDENTIFIER__/files" - } - fun checkPermission (permission: String): Boolean { val status = androidx.core.content.ContextCompat.checkSelfPermission( this.applicationContext, diff --git a/src/android/webview.kt b/src/android/webview.kt index 6986716495..eaca42c2b6 100644 --- a/src/android/webview.kt +++ b/src/android/webview.kt @@ -86,6 +86,11 @@ open class WebChromeClient (activity: MainActivity) : android.webkit.WebChromeCl } } +final class WebViewURLPathResolution (path: String, redirect: Boolean = false) { + val path = path + val redirect = redirect +} + /** * @see https://developer.android.com/reference/kotlin/android/webkit/WebViewClient */ @@ -140,6 +145,67 @@ open class WebViewClient (activity: WebViewActivity) : android.webkit.WebViewCli return true } + fun resolveURLPathForWebView (input: String? = null): WebViewURLPathResolution? { + var path = input ?: return null + val activity = this.activity.get() ?: return null + val assetManager = activity.getAssetManager() ?: return null + val root = activity.getRootDirectory() + + if (path == "/") { + try { + val htmlPath = "index.html" + val stream = assetManager.open(htmlPath) + stream.close() + return WebViewURLPathResolution("/" + htmlPath) + } catch (_: Exception) {} + } + + if (path.startsWith("/")) { + path = path.substring(1, path.length) + } else if (path.startsWith("./")) { + path = path.substring(2, path.length) + } + + try { + val htmlPath = path + val stream = assetManager.open(htmlPath) + stream.close() + return WebViewURLPathResolution("/" + htmlPath) + } catch (_: Exception) {} + + if (path.endsWith("/")) { + try { + val list = assetManager.list(path) + if (list != null && list.size > 0) { + try { + val htmlPath = path + "index.html" + val stream = assetManager.open(htmlPath) + stream.close() + return WebViewURLPathResolution("/" + htmlPath) + } catch (_: Exception) {} + } + } catch (_: Exception) {} + + return null + } else { + try { + val htmlPath = path + "/index.html" + val stream = assetManager.open(htmlPath) + stream.close() + return WebViewURLPathResolution("/" + htmlPath, true) + } catch (_: Exception) {} + } + + try { + val htmlPath = path + ".html" + val stream = assetManager.open(htmlPath) + stream.close() + return WebViewURLPathResolution("/" + htmlPath) + } catch (_: Exception) {} + + return null + } + override fun shouldInterceptRequest ( view: android.webkit.WebView, request: android.webkit.WebResourceRequest @@ -156,18 +222,18 @@ open class WebViewClient (activity: WebViewActivity) : android.webkit.WebViewCli var path = url.path val regex = Regex("(\\.[a-z|A-Z|0-9|_|-]+)$") var redirect = false + val resolved = resolveURLPathForWebView(path) - if (path != null && !regex.containsMatchIn(path)) { - if (path.endsWith("/")) { - path += "index.html" - } else { - path += "/" - redirect = true - } + if (resolved != null) { + path = resolved.path } - if (redirect) { - val redirectURL = "${url.scheme}://${url.host}${path}" + if (resolved != null && resolved.redirect) { + redirect = true + } + + if (redirect && resolved != null) { + val redirectURL = "${url.scheme}://${url.host}${resolved.path}" val redirectSource = """ <meta http-equiv="refresh" content="0; url='${redirectURL}'" />" """ @@ -385,6 +451,15 @@ open class WebViewActivity : androidx.appcompat.app.AppCompatActivity() { } } + fun getAssetManager (): android.content.res.AssetManager { + return this.applicationContext.resources.assets + } + + open fun getRootDirectory (): String { + return getExternalFilesDir(null)?.absolutePath + ?: "/sdcard/Android/data/__BUNDLE_IDENTIFIER__/files" + } + /** * Called when the `WebViewActivity` is first created * @see https://developer.android.com/reference/kotlin/android/app/Activity#onCreate(android.os.Bundle) diff --git a/src/ipc/bridge.cc b/src/ipc/bridge.cc index d909cd4cee..ac9f5bb9da 100644 --- a/src/ipc/bridge.cc +++ b/src/ipc/bridge.cc @@ -2212,14 +2212,6 @@ static void registerSchemeHandler (Router *router) { auto ext = fs::path(path).extension().string(); - if (path == "/" || path.size() == 0) { - path = "/index.html"; - ext = ".html"; - } else if (path.ends_with("/")) { - path += "index.html"; - ext = ".html"; - } - if (ext.size() > 0 && !ext.starts_with(".")) { ext = "." + ext; } @@ -2247,8 +2239,13 @@ static void registerSchemeHandler (Router *router) { return; } - if (ext.size() == 0) { - auto redirectURL = uri + "/"; + auto resolved = Router::resolveURLPathForWebView(path, cwd); + path = resolved.path; + + if (path.size() == 0 && userConfig.contains("webview_default_index")) { + path = userConfig["webview_default_index"]; + } else if (resolved.redirect) { + auto redirectURL = path; auto redirectSource = String( "<meta http-equiv=\"refresh\" content=\"0; url='" + redirectURL + "'\" />" ); @@ -2270,9 +2267,11 @@ static void registerSchemeHandler (Router *router) { return; } - path = fs::absolute(fs::path(cwd) / path.substr(1)).string(); + if (path.size() > 0) { + path = fs::absolute(fs::path(cwd) / path.substr(1)).string(); + } - if (!fs::exists(path)) { + if (path.size() == 0 || !fs::exists(path)) { auto stream = g_memory_input_stream_new_from_data(nullptr, 0, 0); auto response = webkit_uri_scheme_response_new(stream, 0); @@ -2380,6 +2379,7 @@ static void registerSchemeHandler (Router *router) { NSData* data = nullptr; bool isModule = false; + auto basePath = String(NSBundle.mainBundle.resourcePath.UTF8String); auto path = String(components.path.UTF8String); auto ext = String( @@ -2388,14 +2388,6 @@ static void registerSchemeHandler (Router *router) { : "" ); - if (path == "/" || path.size() == 0) { - path = "/index.html"; - ext = ".html"; - } else if (path.ends_with("/")) { - path += "index.html"; - ext = ".html"; - } - if (ext.size() > 0 && !ext.starts_with(".")) { ext = "." + ext; } @@ -2404,10 +2396,31 @@ static void registerSchemeHandler (Router *router) { host.UTF8String != nullptr && String(host.UTF8String) == bundleIdentifier ) { - if (ext.size() == 0 && userConfig.contains("webview_default_index")) { - path = userConfig["webview_default_index"]; - } else if (ext.size() == 0) { - auto redirectURL = String(request.URL.absoluteString.UTF8String) + "/"; + auto resolved = Router::resolveURLPathForWebView(path, basePath); + path = resolved.path; + + if (path.size() == 0) { + if (userConfig.contains("webview_default_index")) { + path = userConfig["webview_default_index"]; + } else { + auto response = [[NSHTTPURLResponse alloc] + initWithURL: request.URL + statusCode: 404 + HTTPVersion: @"HTTP/1.1" + headerFields: headers + ]; + + [task didReceiveResponse: response]; + [task didReceiveData: data]; + [task didFinish]; + + #if !__has_feature(objc_arc) + [response release]; + #endif + return; + } + } else if (resolved.redirect) { + auto redirectURL = path; auto redirectSource = String( "<meta http-equiv=\"refresh\" content=\"0; url='" + redirectURL + "'\" />" ); @@ -3202,6 +3215,74 @@ namespace SSC::IPC { } } + /* + + . + โ”œโ”€โ”€ a-conflict-index + โ”‚ โ””โ”€โ”€ index.html + โ”œโ”€โ”€ a-conflict-index.html + โ”œโ”€โ”€ an-index-file + โ”‚ โ”œโ”€โ”€ a-html-file.html + โ”‚ โ””โ”€โ”€ index.html + โ”œโ”€โ”€ another-file.html + โ””โ”€โ”€ index.html + + Subtleties: + Direct file navigation always wins + /foo/index.html have precedent over foo.html + /foo redirects to /foo/ when there is a /foo/index.html + + '/' -> '/index.html' + '/index.html' -> '/index.html' + '/a-conflict-index' -> redirect to '/a-conflict-index/' + '/another-file' -> '/another-file.html' + '/another-file.html' -> '/another-file.html' + '/an-index-file/' -> '/an-index-file/index.html' + '/an-index-file' -> redirect to '/an-index-file/' + '/an-index-file/a-html-file' -> '/an-index-file/a-html-file.html' + */ + Router::WebViewURLPathResolution Router::resolveURLPathForWebView (String inputPath, const String& basePath) { + namespace fs = std::filesystem; + + if (inputPath.starts_with("/")) { + inputPath = inputPath.substr(1); + } + + // Resolve the full path + fs::path fullPath = fs::path(basePath) / fs::path(inputPath); + + // 1. Try the given path if it's a file + if (fs::is_regular_file(fullPath)) { + return Router::WebViewURLPathResolution{"/" + fs::relative(fullPath, basePath).string()}; + } + + // 2. Try appending a `/` to the path and checking for an index.html + fs::path indexPath = fullPath / fs::path("index.html"); + if (fs::is_regular_file(indexPath)) { + if (fullPath.string().ends_with("/")) { + return Router::WebViewURLPathResolution{ + .path = "/" + fs::relative(indexPath, basePath).string(), + .redirect = false + }; + } else { + return Router::WebViewURLPathResolution{ + .path = "/" + fs::relative(fullPath, basePath).string() + "/", + .redirect = true + }; + } + } + + // 3. Check if appending a .html file extension gives a valid file + fs::path htmlPath = fullPath; + htmlPath.replace_extension(".html"); + if (fs::is_regular_file(htmlPath)) { + return Router::WebViewURLPathResolution{"/" + fs::relative(htmlPath, basePath).string()}; + } + + // If no valid path is found, return empty string + return Router::WebViewURLPathResolution{}; + }; + Router::Router () { static auto userConfig = SSC::getUserConfig(); diff --git a/src/ipc/ipc.hh b/src/ipc/ipc.hh index b4fcbe6550..2638559bf6 100644 --- a/src/ipc/ipc.hh +++ b/src/ipc/ipc.hh @@ -228,6 +228,13 @@ namespace SSC::IPC { using Table = std::map<String, MessageCallbackContext>; using Listeners = std::map<String, std::vector<MessageCallbackListenerContext>>; + struct WebViewURLPathResolution { + String path = ""; + bool redirect = false; + }; + + static WebViewURLPathResolution resolveURLPathForWebView (String inputPath, const String& basePath); + private: Table preserved; diff --git a/src/window/win.cc b/src/window/win.cc index 183a38578e..889c387752 100644 --- a/src/window/win.cc +++ b/src/window/win.cc @@ -944,14 +944,6 @@ namespace SSC { auto ext = fs::path(path).extension().string(); - if (path == "/" || path.size() == 0) { - path = "/index.html"; - ext = ".html"; - } else if (path.ends_with("/")) { - path += "index.html"; - ext = ".html"; - } - if (ext.size() > 0 && !ext.starts_with(".")) { ext = "." + ext; } @@ -1015,28 +1007,34 @@ namespace SSC { } } else { auto rootPath = this->modulePath.parent_path(); + auto resolved = IPC::Router::resolveURLPathForWebView(path, rootPath.string()); + path = resolved.path; - if (ext.size() == 0) { + if (path.size() == 0 && userConfig.contains("webview_default_index")) { + path = userConfig["webview_default_index"]; + } else if (resolved.redirect) { uri += "/"; - app.dispatch([&, uri, path, args, deferral, env] { ICoreWebView2WebResourceResponse* res = nullptr; env->CreateWebResourceResponse( nullptr, 301, L"Moved Permanently", WString( - convertStringToWString("Location: ") + convertStringToWString(uri) + L"\n" + - convertStringToWString("Content-Location: ") + convertStringToWString(uri) + L"\n" + convertStringToWString("Location: ") + convertStringToWString(path) + L"\n" + + convertStringToWString("Content-Location: ") + convertStringToWString(path) + L"\n" ).c_str(), &res ); + args->put_Response(res); deferral->Complete(); - }); - return S_OK; + return S_OK; + } + + if (path.size() > 0) { + path = fs::absolute(rootPath / path.substr(1)).string(); } - path = fs::absolute(rootPath / path.substr(1)).string(); LARGE_INTEGER fileSize; auto handle = CreateFile(path.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL); auto getSizeResult = GetFileSizeEx(handle, &fileSize); diff --git a/test/src/index.js b/test/src/index.js index e3c368acf5..3b9c99e782 100644 --- a/test/src/index.js +++ b/test/src/index.js @@ -21,3 +21,4 @@ import './test.js' import './enumeration.js' import './language.js' import './i18n.js' +import './router-resolution.js' diff --git a/test/src/router-resolution.js b/test/src/router-resolution.js new file mode 100644 index 0000000000..81bf864c7a --- /dev/null +++ b/test/src/router-resolution.js @@ -0,0 +1,86 @@ +import test from 'socket:test' +import URL from 'socket:url' + +const basePath = 'router-resolution' +const dirname = URL.resolve(import.meta.url, basePath) + +test('router-resolution', async (t) => { + const response = await fetch(dirname + '/invalid') + t.equal(response.status, 404, '404 on invalid route') + + const cases = [ + { + url: '/', + bodyTest: '/index.html', + redirect: false + }, + { + url: '/index.html', + bodyTest: '/index.html', + redirect: false + }, + { + url: '/a-conflict-index', + bodyTest: '/a-conflict-index/index.html', + redirect: true, + redirectBodyTest: "<meta http-equiv=\"refresh\" content=\"0; url='/router-resolution/a-conflict-index/'\" />" + }, + { + url: '/another-file', + bodyTest: '/another-file.html', + redirect: false + }, + { + url: '/another-file.html', + bodyTest: '/another-file.html', + redirect: false + }, + { + url: '/an-index-file/', + bodyTest: '/an-index-file/index.html', + redirect: false + }, + { + url: '/an-index-file', + bodyTest: '/an-index-file/index.html', + redirect: true, + redirectBodyTest: "<meta http-equiv=\"refresh\" content=\"0; url='/router-resolution/an-index-file/'\" />" + }, + { + url: '/an-index-file/a-html-file', + bodyTest: '/an-index-file/a-html-file.html', + redirect: false + }, + { + url: '/an-index-file/a-html-file.html', + bodyTest: '/an-index-file/a-html-file.html', + redirect: false + } + ] + + for (const testCase of cases) { + const requestUrl = dirname + testCase.url + const response = await fetch(requestUrl) + const responseBody = (await response.text()).trim() + + t.ok(response.ok, `response is ok for ${testCase.url}`) + t.ok(response.status, `response status is 200 for ${testCase.url}`) + + if (testCase.redirect) { + t.equal(responseBody, testCase.redirectBodyTest, `Redirect response body matches ${testCase.redirectBodyTest}`) + const extractedRedirectURL = extractUrl(responseBody) + + const redirectResponse = await fetch(extractedRedirectURL) + const redirectResponseBody = (await redirectResponse.text()).trim() + t.equal(redirectResponseBody, testCase.bodyTest, `Redirect response body matches ${testCase.bodyTest}`) + } else { + t.equal(responseBody, testCase.bodyTest, `response body matches ${testCase.bodyTest}`) + } + } +}) + +function extractUrl (content) { + const regex = /url\s*=\s*(["'])([^"']+)\1/i + const match = content.match(regex) + return match ? match[2] : null +} diff --git a/test/src/router-resolution/a-conflict-index.html b/test/src/router-resolution/a-conflict-index.html new file mode 100644 index 0000000000..b902ec4798 --- /dev/null +++ b/test/src/router-resolution/a-conflict-index.html @@ -0,0 +1 @@ +/a-conflict-index.html diff --git a/test/src/router-resolution/a-conflict-index/index.html b/test/src/router-resolution/a-conflict-index/index.html new file mode 100644 index 0000000000..b95b257484 --- /dev/null +++ b/test/src/router-resolution/a-conflict-index/index.html @@ -0,0 +1 @@ +/a-conflict-index/index.html diff --git a/test/src/router-resolution/an-index-file/a-html-file.html b/test/src/router-resolution/an-index-file/a-html-file.html new file mode 100644 index 0000000000..afb8bacc0c --- /dev/null +++ b/test/src/router-resolution/an-index-file/a-html-file.html @@ -0,0 +1 @@ +/an-index-file/a-html-file.html diff --git a/test/src/router-resolution/an-index-file/index.html b/test/src/router-resolution/an-index-file/index.html new file mode 100644 index 0000000000..4eab047586 --- /dev/null +++ b/test/src/router-resolution/an-index-file/index.html @@ -0,0 +1 @@ +/an-index-file/index.html diff --git a/test/src/router-resolution/another-file.html b/test/src/router-resolution/another-file.html new file mode 100644 index 0000000000..6e50ed5f21 --- /dev/null +++ b/test/src/router-resolution/another-file.html @@ -0,0 +1 @@ +/another-file.html diff --git a/test/src/router-resolution/index.html b/test/src/router-resolution/index.html new file mode 100644 index 0000000000..e90722f8a2 --- /dev/null +++ b/test/src/router-resolution/index.html @@ -0,0 +1 @@ +/index.html diff --git a/test/src/router-resolution/pages/index.html b/test/src/router-resolution/pages/index.html new file mode 100644 index 0000000000..b135adceb1 --- /dev/null +++ b/test/src/router-resolution/pages/index.html @@ -0,0 +1 @@ +/pages/index.html From 005100fbcb25c0d5532bb6422af8bcc1956c6a84 Mon Sep 17 00:00:00 2001 From: Joseph Werle <jwerle@users.noreply.github.com> Date: Mon, 16 Oct 2023 22:15:58 +0200 Subject: [PATCH 207/256] fix(cli.cc): fix push notification permission Enable system push notifications on iOS/macOS with correct entitlements: - com.apple.developer.location.push - com.apple.developer.usernotifications.filtering These SHOULD be enabled with the `[permissions] allow_push_notifications` permission, which is `default` by default --- src/cli/cli.cc | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 4ac77ccab2..5a9d19142e 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -4510,21 +4510,21 @@ int main (const int argc, const char* argv[]) { ); } - if (settings["permissions_allow_notifications"] != "false") { + if (settings["permissions_allow_push_notifications"] != "false") { entitlementSettings["configured_entitlements"] += ( " <key>com.apple.developer.usernotifications.filtering</key>\n" " <true/>\n" ); - } - - if (settings["permissions_allow_geolocation"] != "false") { + entitlementSettings["configured_entitlements"] += ( - " <key>com.apple.security.personal-information.location</key>\n" + " <key>com.apple.developer.location.push</key>\n" " <true/>\n" ); + } + if (settings["permissions_allow_geolocation"] != "false") { entitlementSettings["configured_entitlements"] += ( - " <key>com.apple.developer.location.push</key>\n" + " <key>com.apple.security.personal-information.location</key>\n" " <true/>\n" ); } From ab086dc67f4d12e8ff265a345c36f3fb14416530 Mon Sep 17 00:00:00 2001 From: Joseph Werle <jwerle@users.noreply.github.com> Date: Mon, 16 Oct 2023 22:17:12 +0200 Subject: [PATCH 208/256] fix(cli.cc): Fix "true" check --- src/cli/cli.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 5a9d19142e..d52618bf3c 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -4510,7 +4510,7 @@ int main (const int argc, const char* argv[]) { ); } - if (settings["permissions_allow_push_notifications"] != "false") { + if (settings["permissions_allow_push_notifications"] == "true") { entitlementSettings["configured_entitlements"] += ( " <key>com.apple.developer.usernotifications.filtering</key>\n" " <true/>\n" From c4b473b8b5b5161cb12c019a7104f0067a07325f Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Tue, 17 Oct 2023 11:47:10 +0200 Subject: [PATCH 209/256] chore(docs): improve config doc --- api/CONFIG.md | 28 ++++++++++++++-------------- bin/docs-generator/config.js | 2 +- bin/publish-npm-modules.sh | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/api/CONFIG.md b/api/CONFIG.md index 62453afe7a..c13beafcc7 100644 --- a/api/CONFIG.md +++ b/api/CONFIG.md @@ -33,7 +33,7 @@ provisioning_profile = "johndoe.mobileprovision" simulator_device = "iPhone 15" ``` -# Section `build` +# `build` Key | Default Value | Description :--- | :--- | :--- @@ -45,13 +45,13 @@ name | | The name of the program and executable to be output. Can't contain sp output | "build" | The binary output path. It's recommended to add this path to .gitignore. script | | The build script. It runs before the `[build] copy` phase. -# Section `build.watch` +# `build.watch` Key | Default Value | Description :--- | :--- | :--- sources | | -# Section `webview` +# `webview` Key | Default Value | Description :--- | :--- | :--- @@ -59,7 +59,7 @@ root | "/" | Make root open index.html default_index | "" | Set default 'index.html' path to open for implicit routes watch | false | Enable watch mode -# Section `permissions` +# `permissions` Key | Default Value | Description :--- | :--- | :--- @@ -75,13 +75,13 @@ allow_bluetooth | true | Allow/Disallow bluetooth in application allow_data_access | true | Allow/Disallow data access in application allow_airplay | true | Allow/Disallow AirPlay access in application (macOS/iOS) only -# Section `debug` +# `debug` Key | Default Value | Description :--- | :--- | :--- flags | | Advanced Compiler Settings for debug purposes (ie C++ compiler -g, etc). -# Section `meta` +# `meta` Key | Default Value | Description :--- | :--- | :--- @@ -95,7 +95,7 @@ title | | The title of the app used in metadata files. This is NOT a window ti type | "" | Builds an extension when set to "extension". version | | A string that indicates the version of the application. It should be a semver triple like 1.2.3. Defaults to 1.0.0. -# Section `android` +# `android` Key | Default Value | Description :--- | :--- | :--- @@ -109,7 +109,7 @@ native_sources | | native_makefile | | sources | | -# Section `ios` +# `ios` Key | Default Value | Description :--- | :--- | :--- @@ -118,7 +118,7 @@ distribution_method | | Describes how Xcode should export the archive. Availab provisioning_profile | | A path to the provisioning profile used for signing iOS app. simulator_device | | which device to target when building for the simulator -# Section `linux` +# `linux` Key | Default Value | Description :--- | :--- | :--- @@ -126,7 +126,7 @@ categories | | Helps to make your app searchable in Linux desktop environments cmd | | The command to execute to spawn the "back-end" process. icon | | The icon to use for identifying your app in Linux desktop environments. -# Section `mac` +# `mac` Key | Default Value | Description :--- | :--- | :--- @@ -137,14 +137,14 @@ icon | | The icon to use for identifying your app on MacOS. codesign_identity | | TODO Signing guide: https://socketsupply.co/guides/#code-signing-certificates codesign_paths | | Additional paths to codesign -# Section `native` +# `native` Key | Default Value | Description :--- | :--- | :--- files | | Files that should be added to the compile step. headers | | Extra Headers -# Section `win` +# `win` Key | Default Value | Description :--- | :--- | :--- @@ -153,14 +153,14 @@ icon | | The icon to use for identifying your app on Windows. logo | | The icon to use for identifying your app on Windows. pfx | | A relative path to the pfx file used for signing. -# Section `window` +# `window` Key | Default Value | Description :--- | :--- | :--- height | | The initial height of the first window. width | | The initial width of the first window. -# Section `headless` +# `headless` Key | Default Value | Description :--- | :--- | :--- diff --git a/bin/docs-generator/config.js b/bin/docs-generator/config.js index 6f65ea51a5..e5e75dbd8d 100644 --- a/bin/docs-generator/config.js +++ b/bin/docs-generator/config.js @@ -72,7 +72,7 @@ simulator_device = "iPhone 15" ` md += '\n' Object.entries(sections).forEach(([sectionName, settings]) => { - md += `# Section \`${sectionName}\`\n` + md += `# \`${sectionName}\`\n` md += '\n' md += 'Key | Default Value | Description\n' md += ':--- | :--- | :---\n' diff --git a/bin/publish-npm-modules.sh b/bin/publish-npm-modules.sh index 74f8bca420..041657a4b1 100755 --- a/bin/publish-npm-modules.sh +++ b/bin/publish-npm-modules.sh @@ -110,7 +110,7 @@ fi declare android_abis=() -if (( only_top_level )); then +if (( !only_platforms || only_top_level )); then npm run gen elif [[ "arm64" == "$(host_arch)" ]] && [[ "linux" == "$platform" ]]; then echo "warn - Android not supported on $platform-"$(uname -m)"" From d153826b5bc2c07be042053946da2e4921827ba3 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Tue, 17 Oct 2023 14:31:41 -0400 Subject: [PATCH 210/256] fix(src/cli): fix 'Info.plist' generation for iOS --- src/cli/cli.cc | 76 ++++++++++++++++++++++++++++++++++++++++++++ src/cli/templates.hh | 22 ------------- 2 files changed, 76 insertions(+), 22 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index d52618bf3c..a39996be05 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -4217,6 +4217,79 @@ int main (const int argc, const char* argv[]) { settings["ios_info_plist_data"] = ""; } + settings["ios_info_plist_data"] += ( + " <key>UIBackgroundModes</key>\n" + " <array>\n" + " <string>fetch</string>\n" + " <string>processing</string>\n" + ); + + if (settings["permissions_allow_bluetooth"] != "false") { + settings["ios_info_plist_data"] += ( + " <string>bluetooth-central</string>\n" + " <string>bluetooth-peripheral</string>\n" + ); + } + + if (settings["permissions_allow_geolocation"] != "false") { + settings["ios_info_plist_data"] += ( + " <string>location</string>\n" + ); + } + + if (settings["permissions_allow_push_notifications"] == "true") { + settings["ios_info_plist_data"] += ( + " <string>remote-notification</string>\n" + ); + } + + settings["ios_info_plist_data"] += ( + " </array>\n" + ); + + settings["ios_info_plist_data"] += ( + " <key>UIRequiredDeviceCapabilities</key>\n" + " <array>\n" + ); + + if (settings["permissions_allow_bluetooth"] != "false") { + settings["ios_info_plist_data"] += ( + " <string>bluetooth-le</string>\n" + ); + } + + if (settings["permissions_allow_geolocation"] != "false") { + settings["ios_info_plist_data"] += ( + " <string>gps</string>\n" + " <string>location-services</string>\n" + ); + } + + if (settings["permissions_allow_sensors"] != "false") { + settings["ios_info_plist_data"] += ( + " <string>accelerometer</string>\n" + " <string>gyroscope</string>\n" + ); + } + + if (settings["permission_allow_user_media"] != "false") { + if (settings["permissions_allow_microphone"] != "false") { + settings["ios_info_plist_data"] += ( + " <string>microphone</string>\n" + ); + } + + if (settings["permissions_allow_camera"] != "false") { + settings["ios_info_plist_data"] += ( + " <string>video-camera</string>\n" + ); + } + } + + settings["ios_info_plist_data"] += ( + " </array>\n" + ); + writeFile(paths.platformSpecificOutputPath / "exportOptions.plist", tmpl(gXCodeExportOptions, settings)); writeFile(paths.platformSpecificOutputPath / "Info.plist", tmpl(gIOSInfoPList, settings)); writeFile(pathToProject / "project.pbxproj", tmpl(gXCodeProject, xCodeProjectVariables)); @@ -4617,6 +4690,9 @@ int main (const int argc, const char* argv[]) { if (rExport.exitCode != 0) { log("ERROR: failed to export project"); + if (flagVerboseMode) { + log(rExport.output); + } fs::current_path(oldCwd); exit(1); } diff --git a/src/cli/templates.hh b/src/cli/templates.hh index a868d41c31..4431d6e282 100644 --- a/src/cli/templates.hh +++ b/src/cli/templates.hh @@ -1158,28 +1158,6 @@ constexpr auto gIOSInfoPList = R"XML(<?xml version="1.0" encoding="UTF-8"?> <key>NSSupportsAutomaticGraphicsSwitching</key> <true/> - <key>UIBackgroundModes</key> - <array> - <string>fetch</string> - <string>processing</string> - <string>location</string> - <string>bluetooth-central</string> - <string>bluetooth-peripheral</string> - <string>remote-notification</string> - </array> - - <key>UIRequiredDeviceCapabilities</key> - <array> - <string>accelerometer</string> - <string>bluetooth-le</string> - <string>gps</string> - <string>gyroscope</string> - <string>location-services</string> - <string>microphone</string> - <string>peer-to-peer</string> - <string>video-camera</string> - </array> - <!-- Permission usage descriptions --> <key>NSAppDataUsageDescription</key> From c4fb2b1e0fe202405a84bec0c58389a3d7a8df7d Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 17 Oct 2023 13:20:28 -0400 Subject: [PATCH 211/256] fix(socket-node): avoid resolving `send` promise with Error object It seemed odd to need to handle errors as a promise result, rather than through a catch callback. --- npm/packages/@socketsupply/socket-node/index.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/npm/packages/@socketsupply/socket-node/index.js b/npm/packages/@socketsupply/socket-node/index.js index c604710d4f..a10ecfacec 100644 --- a/npm/packages/@socketsupply/socket-node/index.js +++ b/npm/packages/@socketsupply/socket-node/index.js @@ -1,5 +1,6 @@ // @ts-check import { EventEmitter } from 'node:events' +import { promisify } from 'node:util' const MAX_MESSAGE_KB = 512 * 1024 @@ -33,7 +34,7 @@ class API { function overrideStreamWrite (stream, write) { const protoWrite = Object.getPrototypeOf(stream).write stream.write = write - return protoWrite.bind(stream) + return promisify(protoWrite.bind(stream)) } this.#writeStdout = overrideStreamWrite( @@ -155,7 +156,7 @@ class API { /** * @param {string} s - * @returns {Promise<Error | undefined>} + * @returns {Promise<void>} * @throws {Error} * @ignore */ @@ -172,7 +173,7 @@ class API { this.#writeStderr('RAW MESSAGE: ' + s.slice(0, 512) + '...\n') } - return new Promise((resolve) => this.#writeStdout(s + '\n', resolve)) + return this.#writeStdout(s + '\n') } // @@ -184,7 +185,7 @@ class API { * @param {number} options.window - window index to send event to * @param {string} options.event - event name * @param {any=} options.value - data to send - * @returns {Promise<Error | undefined>} + * @returns {Promise<void>} * @throws {Error} */ async send (options) { @@ -214,7 +215,7 @@ class API { /** * Send the heartbeat event to the webview. - * @returns {Promise<Error | undefined>} + * @returns {Promise<void>} * @throws {Error} */ async heartbeat () { From 19804c3c8aee7dbcb996d85da5097fe57a007093 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 17 Oct 2023 13:21:44 -0400 Subject: [PATCH 212/256] chore: update api docs --- npm/packages/@socketsupply/socket-node/API.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/npm/packages/@socketsupply/socket-node/API.md b/npm/packages/@socketsupply/socket-node/API.md index 9535c6f146..45b2f53af3 100644 --- a/npm/packages/@socketsupply/socket-node/API.md +++ b/npm/packages/@socketsupply/socket-node/API.md @@ -1,4 +1,4 @@ -### [`send(options)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L190) +### [`send(options)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L191) Send event to webview via IPC @@ -11,17 +11,17 @@ Send event to webview via IPC | Return Value | Type | Description | | :--- | :--- | :--- | -| Not specified | Promise<Error \| undefined> | | +| Not specified | Promise<void> | | -### [`heartbeat()`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L220) +### [`heartbeat()`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L221) Send the heartbeat event to the webview. | Return Value | Type | Description | | :--- | :--- | :--- | -| Not specified | Promise<Error \| undefined> | | +| Not specified | Promise<void> | | -### [`addListener(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L231) +### [`addListener(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L232) Adds a listener to the window. @@ -30,7 +30,7 @@ Adds a listener to the window. | event | string | | false | the event to listen to | | cb | function(*): void | | false | the callback to call | -### [`on(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L242) +### [`on(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L243) Adds a listener to the window. An alias for `addListener`. @@ -39,7 +39,7 @@ Adds a listener to the window. An alias for `addListener`. | event | string | | false | the event to listen to | | cb | function(*): void | | false | the callback to call | -### [`once(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L252) +### [`once(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L253) Adds a listener to the window. The listener is removed after the first call. @@ -48,7 +48,7 @@ Adds a listener to the window. The listener is removed after the first call. | event | string | | false | the event to listen to | | cb | function(*): void | | false | the callback to call | -### [`removeListener(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L262) +### [`removeListener(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L263) Removes a listener from the window. @@ -57,7 +57,7 @@ Removes a listener from the window. | event | string | | false | the event to remove the listener from | | cb | function(*): void | | false | the callback to remove | -### [`removeAllListeners(event)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L271) +### [`removeAllListeners(event)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L272) Removes all listeners from the window. @@ -65,7 +65,7 @@ Removes all listeners from the window. | :--- | :--- | :---: | :---: | :--- | | event | string | | false | the event to remove the listeners from | -### [`off(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L282) +### [`off(event, cb)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L283) Removes a listener from the window. An alias for `removeListener`. From e5dfe3a90120f912c2e6cfb5bf5759f5f7163be7 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Wed, 18 Oct 2023 11:43:05 -0400 Subject: [PATCH 213/256] fix(socket-node): include stack trace for uncaught exceptions --- npm/packages/@socketsupply/socket-node/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm/packages/@socketsupply/socket-node/index.js b/npm/packages/@socketsupply/socket-node/index.js index a10ecfacec..6d07571566 100644 --- a/npm/packages/@socketsupply/socket-node/index.js +++ b/npm/packages/@socketsupply/socket-node/index.js @@ -28,7 +28,7 @@ class API { this.#write(`ipc://process.exit?value=${exitCode}`) }) process.on('uncaughtException', (err) => { - this.#write(`ipc://stderr?value=${encodeURIComponent(String(err))}`) + this.#write(`ipc://stderr?value=${encodeURIComponent(err.stack || String(err))}`) }) function overrideStreamWrite (stream, write) { From fbf0b23af5f74010a42bf84c0c2f9bb261327aa6 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Fri, 13 Oct 2023 13:02:21 -0400 Subject: [PATCH 214/256] refactor(): initial runtime core test setup --- bin/generate-compile-flags-txt.sh | 2 +- include/socket/extension.h | 10 +- package.json | 1 + src/core/codec.hh | 39 ++++ src/core/config.cc | 90 ++++++++ src/core/config.hh | 105 +++++++++ src/core/ini.cc | 6 +- src/core/ini.hh | 1 + test/clib.json | 8 + test/deps/ok/Makefile | 102 +++++++++ test/deps/ok/clib.json | 16 ++ test/deps/ok/ok.c | 2 + test/deps/ok/ok.h | 356 ++++++++++++++++++++++++++++++ test/package.json | 1 + test/socket.ini | 1 + test/src/runtime-core/codec.cc | 82 +++++++ test/src/runtime-core/config.cc | 9 + test/src/runtime-core/env.cc | 6 + test/src/runtime-core/harness.cc | 137 ++++++++++++ test/src/runtime-core/ini.cc | 6 + test/src/runtime-core/json.cc | 6 + test/src/runtime-core/main.cc | 18 ++ test/src/runtime-core/main.js | 10 + test/src/runtime-core/ok.cc | 3 + test/src/runtime-core/ok.hh | 11 + test/src/runtime-core/platform.cc | 6 + test/src/runtime-core/preload.cc | 6 + test/src/runtime-core/socket.ini | 27 +++ test/src/runtime-core/tests.hh | 51 +++++ test/src/runtime-core/version.cc | 6 + 30 files changed, 1117 insertions(+), 7 deletions(-) create mode 100644 src/core/config.cc create mode 100644 test/clib.json create mode 100644 test/deps/ok/Makefile create mode 100644 test/deps/ok/clib.json create mode 100644 test/deps/ok/ok.c create mode 100644 test/deps/ok/ok.h create mode 100644 test/src/runtime-core/codec.cc create mode 100644 test/src/runtime-core/config.cc create mode 100644 test/src/runtime-core/env.cc create mode 100644 test/src/runtime-core/harness.cc create mode 100644 test/src/runtime-core/ini.cc create mode 100644 test/src/runtime-core/json.cc create mode 100644 test/src/runtime-core/main.cc create mode 100644 test/src/runtime-core/main.js create mode 100644 test/src/runtime-core/ok.cc create mode 100644 test/src/runtime-core/ok.hh create mode 100644 test/src/runtime-core/platform.cc create mode 100644 test/src/runtime-core/preload.cc create mode 100644 test/src/runtime-core/socket.ini create mode 100644 test/src/runtime-core/tests.hh create mode 100644 test/src/runtime-core/version.cc diff --git a/bin/generate-compile-flags-txt.sh b/bin/generate-compile-flags-txt.sh index 6cd1a46b0a..b3379a96b6 100755 --- a/bin/generate-compile-flags-txt.sh +++ b/bin/generate-compile-flags-txt.sh @@ -60,7 +60,7 @@ while (( $# > 0 )); do done function generate () { - local cflags=($("$root/bin/cflags.sh" "${args[@]}") -ferror-limit=0 -I"$root/src") + local cflags=($("$root/bin/cflags.sh" "${args[@]}") -ferror-limit=0 -I"$root/src" -I"$root") for flag in "${cflags[@]}"; do echo "$flag" diff --git a/include/socket/extension.h b/include/socket/extension.h index 7283d43d63..1c6392ed7a 100644 --- a/include/socket/extension.h +++ b/include/socket/extension.h @@ -795,11 +795,11 @@ extern "C" { * @param format - Format string for formatted output * @param ... - `format` argument values */ - #define sapi_printf(ctx, format, ...) ({ \ - char _buffer[BUFSIZ] = {0}; \ - int _size = snprintf(NULL, 0, format, ##__VA_ARGS__) + 1; \ - snprintf(_buffer, _size, format, ##__VA_ARGS__); \ - sapi_log(ctx, _buffer); \ + #define sapi_printf(ctx, format, ...) ({ \ + char _buffer[BUFSIZ] = {0}; \ + int _size = snprintf(NULL, 0, format, ##__VA_ARGS__) + 1; \ + snprintf(_buffer, _size, format, ##__VA_ARGS__); \ + sapi_log(ctx, _buffer); \ }) /** diff --git a/package.json b/package.json index cd3259f2ba..1ef8ce027e 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "gen:docs": "node ./bin/generate-docs.js", "gen:tsc": "./bin/generate-typescript-typings.sh", "test": "cp -f .ssc.env test | echo && cd test && npm install --silent --no-audit && npm test", + "test:runtime-core": "cd test && npm run test:runtime-core", "test:lint": "standard .", "test:node": "node ./test/node/index.js", "test:ios-simulator": "cd test && npm install --silent --no-audit && npm run test:ios-simulator", diff --git a/src/core/codec.hh b/src/core/codec.hh index 7652993bc2..6ab89360be 100644 --- a/src/core/codec.hh +++ b/src/core/codec.hh @@ -4,12 +4,51 @@ #include "types.hh" namespace SSC { + /** + * Encodes input by replacing certain characters by + * one, two, three, or four escape sequences representing the UTF-8 + * encoding of the character. + * @param input The input string to encode + * @return An encoded string value + */ String encodeURIComponent (const String& input); + + /** + * Decodes a value encoded with `encodeURIComponent` + * @param input The input string to decode + * @return A decoded string + */ String decodeURIComponent (const String& input); + + /** + * Encodes input as a string of hex characters. + * @param input The input string to encode + * @return An encoded string value + */ String encodeHexString (const String& input); + + /** + * Decodes input as a string of hex characters to a normal string. + * @param input The input string to encode + * @return An encoded string value + */ String decodeHexString (const String& input); + + /** + * Decodes UTF8 byte string of variable `length` size in `input` to + * `output` returning `size_t` bytes written to `output`. + * @param output Pointer owned by caller to write decoded output to + * @param input Pointer owned by caller to decode `length` bytes + * @param length Size of `input` in bytes + * @return The number of bytes written to `output` + */ size_t decodeUTF8 (char *output, const char *input, size_t length); + /** + * Converts a `uint64_t` to a array of `uint8_t` values (bytes) + * @param input The `uint64_t` to convert to an array of bytes + * @return An array of `uint8_t` values + */ const Array<uint8_t, 8> toBytes (const uint64_t input); } diff --git a/src/core/config.cc b/src/core/config.cc new file mode 100644 index 0000000000..57ff1eaba4 --- /dev/null +++ b/src/core/config.cc @@ -0,0 +1,90 @@ +#include "config.hh" +#include "ini.hh" +#include "string.hh" + +namespace SSC { + static const String NAMESPACE_SEPARATOR = "_"; + + Config::Config (const String& source) { + this->map = INI::parse(source); + } + + Config::Config (const Config& source) { + this->map = source.data(); + } + + Config::Config (const Map& source) { + this->map = source; + } + + const String Config::get (const String& key) const { + if (this->contains(key)) { + return this->at(key); + } + + return ""; + } + + const String& Config::at (const String& key) const { + return this->map.at(key); + } + + void Config::set (const String& key, const String& value) { + this->map.insert_or_assign(key, value); + } + + bool Config::contains (const String& key) const { + if (this->map.contains(key) && this->map.at(key).size() > 0) { + return true; + } + + for (const auto& tuple : this->map) { + if ( + tuple.first.starts_with(key + NAMESPACE_SEPARATOR) && + tuple.second.size() > 0 + ) { + return true; + } + } + + return false; + } + + bool Config::erase (const String& key) { + if (this->map.contains(key)) { + this->map.erase(key); + return true; + } + + return false; + } + + const Map& Config::data () const { + return this->map; + } + + const Config Config::slice (const String& key) const { + Map slice; + + for (const auto& tuple : this->map) { + if ( + tuple.first.starts_with(key + NAMESPACE_SEPARATOR) && + tuple.second.size() > 0 + ) { + const auto k = tuple.first.substr(key.size() + 1, tuple.first.size()); + const auto v = tuple.second; + slice.insert_or_assign(k, v); + } + } + + return Config { slice }; + } + + const String Config::operator [] (const String& key) const { + return this->map.at(key); + } + + const String& Config::operator [] (const String& key) { + return this->map[key]; + } +} diff --git a/src/core/config.hh b/src/core/config.hh index 890d29f44b..7a2ae81884 100644 --- a/src/core/config.hh +++ b/src/core/config.hh @@ -1,6 +1,8 @@ #ifndef SSC_CORE_CONFIG_H #define SSC_CORE_CONFIG_H +#include <iterator> + // TODO(@jwerle): remove this and any need for it #ifndef SSC_SETTINGS #define SSC_SETTINGS "" @@ -43,6 +45,109 @@ namespace SSC { extern bool isDebugEnabled (); extern const char* getDevHost (); extern int getDevPort (); + + /** + * A container for configuration that can be mutated and queried using + * `.` syntax. Configuration can be created from an INI source string. + */ + class Config { + /** + * Internal configuration mapping, exposed as a const reference to the + * caller in `Config::data()` + */ + Map map; + public: + using Iterator = Map::iterator; + + /** + * `Config` class constructors. + */ + Config () = default; + Config (const Map& source); + Config (const String& source); + Config (const Config& source); + + /** + * Get a configuration value by name or `.` path. + * @param key The configuration name or key path + * @return The value at `key` or an empty string. + */ + const String get (const String& key) const; + + /** + * Get a configuration value reference by name. + * @param key The configuration name or key path + * @return `String&` The reference at `key` or an empty string. + */ + const String& at (const String& key) const; + + /** + * Set a configuration `value` by `key`. + * @param key The configuration name of `value` to set + * @param value The value of `key` to set + */ + void set (const String& key, const String& value); + + /** + * Returns `true` if `key` exists in configuration and is not empty. + * @param key The key to check for existence. + * @return `true` if it exists, otherwise `false` + */ + bool contains (const String& key) const; + + /** + * Erase a configuration value by `key`. + * @param key The key to erase a value for.. + * @return `true` if erased, otherwise `false` + */ + bool erase (const String& key); + + /** + * Get a const reference to the underlying data map + * of this configuration. + * @return `Map&` A reference to the internal data map + */ + const Map& data () const; + + /** + * Get a `Config` instance as a "slice" of this configuration, such as + * a subsection using `.` syntax or configuration section prefixes. + * @param key The key to filter on + * @return The value at `key` + * @example + * const auto config = Config::getUserConfig(); + * const auto build = config.slice("build"); + * const auto script = build["script"]; + */ + const Config slice (const String& key) const; + + /** + * Get a configuration value by name or `.` path using `[]` notation. + * @param key The key to look up a value for. + * @return The value at `key` + */ + const String operator [] (const String& key) const; + + /** + * Get a configuration value reference by name or `.` path + * using `[]` notation. + * @param key The key to look up a value for. + * @return A reference to the value at `key` + */ + const String& operator [] (const String& key); + + /** + * Get the beginning of iterator to the configuration tuples. + * @return `Iterator` + */ + Iterator begin (); + + /** + * Get the end of iterator to the configuration tuples. + * @return `Iterator` + */ + Iterator end (); + }; } #endif diff --git a/src/core/ini.cc b/src/core/ini.cc index e035af3839..8c04e6eb2b 100644 --- a/src/core/ini.cc +++ b/src/core/ini.cc @@ -2,6 +2,10 @@ namespace SSC::INI { Map parse (const String& source) { + return parse(source, "_"); + } + + Map parse (const String& source, const String& keyPathSeparator) { Vector<String> entries = split(source, '\n'); String prefix = ""; Map settings = {}; @@ -23,7 +27,7 @@ namespace SSC::INI { prefix = replace(prefix, "\\.", "_"); if (prefix.size() > 0) { - prefix += "_"; + prefix += keyPathSeparator; } continue; diff --git a/src/core/ini.hh b/src/core/ini.hh index 48e00788e8..138a75391a 100644 --- a/src/core/ini.hh +++ b/src/core/ini.hh @@ -6,5 +6,6 @@ namespace SSC::INI { Map parse (const String& source); + Map parse (const String& source, const String& keyPathSeparator); } #endif diff --git a/test/clib.json b/test/clib.json new file mode 100644 index 0000000000..0e499b34cc --- /dev/null +++ b/test/clib.json @@ -0,0 +1,8 @@ +{ + "name": "@socketsupply/socket", + "private": true, + "src": [], + "dependencies": { + "jwerle/libok": "0.6.3" + } +} diff --git a/test/deps/ok/Makefile b/test/deps/ok/Makefile new file mode 100644 index 0000000000..f7eae49df9 --- /dev/null +++ b/test/deps/ok/Makefile @@ -0,0 +1,102 @@ +PREFIX ?= /usr/local +DESTDIR ?= ok + +OS = $(shell uname) +CC ?= clang +AR = ar +LN = ln +RM = rm +VALGRIND ?= valgrind +STRIP = strip + +LIB_NAME = ok +VERSION_MAJOR = 0 +VERSION_MINOR = 6 + +TARGET_NAME = lib$(LIB_NAME) +TARGET_STATIC = $(TARGET_NAME).a +TARGET_DSOLIB = $(TARGET_NAME).so.$(VERSION_MAJOR).$(VERSION_MINOR) +TARGET_DYLIB = $(TARGET_NAME).$(VERSION_MAJOR).$(VERSION_MINOR).dylib +TARGET_DSO = $(TARGET_NAME).so + +CFLAGS += -I. +CFLAGS += -std=c99 -Wall -O2 +CFLAGS += -fvisibility=hidden +CFLAGS += -fPIC -pedantic + +LDFLAGS += -shared -soname $(TARGET_DSO).$(VERSION_MAJOR) + +OSX_LDFLAGS += -lc -Wl,-install_name,$(TARGET_DSO) -o $(TARGET_DSOLIB) + +SRC = $(wildcard *.c) +OBJS = $(SRC:.c=.o) + +TEST_MAIN = ok-test + +ifneq ("Darwin","$(OS)") + CFLAGS += -lm +endif + +ifdef DEBUG + CFLAGS += -DOK_DEBUG +endif + +all: $(TARGET_STATIC) $(TARGET_DSO) + +$(TARGET_STATIC): $(OBJS) + @echo " LIBTOOL-STATIC $(TARGET_STATIC)" + @$(AR) crus $(TARGET_STATIC) $(OBJS) + +$(TARGET_DSO): $(OBJS) + @echo " LIBTOOL-SHARED $(TARGET_DSOLIB)" + @echo " LIBTOOL-SHARED $(TARGET_DSO)" + @echo " LIBTOOL-SHARED $(TARGET_DSO).$(VERSION_MAJOR)" +ifeq ("Darwin","$(OS)") + @$(CC) -shared $(OBJS) $(OSX_LDFLAGS) -o $(TARGET_DSOLIB) + @$(LN) -s $(TARGET_DSOLIB) $(TARGET_DSO) + @$(LN) -s $(TARGET_DSOLIB) $(TARGET_DSO).$(VERSION_MAJOR) +else + @$(CC) -shared $(OBJS) -o $(TARGET_DSOLIB) + @$(LN) -s $(TARGET_DSOLIB) $(TARGET_DSO) + @$(LN) -s $(TARGET_DSOLIB) $(TARGET_DSO).$(VERSION_MAJOR) + @$(STRIP) --strip-unneeded $(TARGET_DSO) +endif + +$(OBJS): + @echo " CC(target) $@" + @$(CC) $(CFLAGS) -c -o $@ $(@:.o=.c) + +check: test + $(VALGRIND) --leak-check=full ./$(TEST_MAIN) + +test: + @echo " LINK(target) ($(TEST_MAIN))" + @$(CC) test.c ./$(TARGET_STATIC) $(CFLAGS) -o $(TEST_MAIN) + @./$(TEST_MAIN) + +clean: + @for item in \ + $(TEST_MAIN) $(OBJS) $(TARGET_STATIC) \ + $(TARGET_DSOLIB) $(TARGET_DSO).$(VERSION_MAJOR) \ + $(TARGET_DSO) $(TARGET_DYLIB) \ + ; do \ + echo " RM $$item"; \ + $(RM) -rf $$item; \ + done; + + +install: all + @test -d $(PREFIX)/$(DESTDIR) || mkdir $(PREFIX)/$(DESTDIR) + @cp *.h $(PREFIX)/include/$(DESTDIR) + @echo " INSTALL $(LIB_NAME).h"; + @install $(LIB_NAME).h $(PREFIX)/include + @echo " INSTALL $(TARGET_STATIC)"; + @install $(TARGET_STATIC) $(PREFIX)/lib + @echo " INSTALL $(TARGET_DSO)"; + @install $(TARGET_DSO) $(PREFIX)/lib + +uninstall: + rm -rf $(PREFIX)/$(DESTDIR) + rm -f $(PREFIX)/lib/$(TARGET_STATIC) + rm -f $(PREFIX)/lib/$(TARGET_DSO) + diff --git a/test/deps/ok/clib.json b/test/deps/ok/clib.json new file mode 100644 index 0000000000..9be4876531 --- /dev/null +++ b/test/deps/ok/clib.json @@ -0,0 +1,16 @@ +{ + "name": "ok", + "version": "0.6.3", + "author": "Joseph Werle", + "description": "Super tiny tap output library", + "repo": "jwerle/libok", + "src": [ + "ok.h", + "ok.c", + "Makefile" + ], + "keywords": [ + "tap", + "test" + ] +} diff --git a/test/deps/ok/ok.c b/test/deps/ok/ok.c new file mode 100644 index 0000000000..eb88bcd494 --- /dev/null +++ b/test/deps/ok/ok.c @@ -0,0 +1,2 @@ +#define LIBOK_INCLUDE_IMPLEMENTATION +#include "ok.h" diff --git a/test/deps/ok/ok.h b/test/deps/ok/ok.h new file mode 100644 index 0000000000..a8b9886236 --- /dev/null +++ b/test/deps/ok/ok.h @@ -0,0 +1,356 @@ +/** + * `ok.h` - libok + * + * Copyright (C) 2014-2023 Joseph Werle <joseph.werle@gmail.com> + */ + +#ifndef OK_H +#define OK_H + +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * libok version + */ +#ifndef OK_VERSION +#define OK_VERSION "0.6.3" +#endif + +/** + * No-op/void `ok()` function + */ +#ifndef okx +#define okx(...) (void) (0); +#endif + +/** + * Allow for custom `printf` implementation. + */ +#if !defined(LIBOK_PRINTF) +#define LIBOK_PRINTF(...) (printf(__VA_ARGS__)) +#endif + +/** + * Allow for custom `fprintf` implementation. + */ +#if !defined(LIBOK_FPRINTF) +#define LIBOK_FPRINTF(...) (fprintf(__VA_ARGS__)) +#endif + +/** + * Configure the need for a newline ('\n') in a `printf` call. + */ +#if !defined(LIBOK_PRINTF_NEEDS_NEWLINE) +#define LIBOK_PRINTF_NEEDS_NEWLINE 1 +#endif + +/** + * Configure the need for a newline ('\n') in a `fprintf` call. + */ +#if !defined(LIBOK_FPRINTF_NEEDS_NEWLINE) +#define LIBOK_FPRINTF_NEEDS_NEWLINE LIBOK_PRINTF_NEEDS_NEWLINE +#endif + +/** + * Increments ok count and + * outputs a message to stdout + */ +#define ok(format, ...) ({ \ + if (ok_count() == 0 && ok_failed() == 0) ok_begin(NULL); \ + int count = ok_count_inc() + ok_failed(); \ + LIBOK_PRINTF("ok %d - " format, count, ##__VA_ARGS__); \ + if (LIBOK_PRINTF_NEEDS_NEWLINE) { \ + LIBOK_PRINTF("\n"); \ + } \ +}) + +/** + * Outputs a a "not ok" message to stdout. + * Increments not ok count. + */ +#define notok(format, ...) ({ \ + if (ok_count() == 0 && ok_failed() == 0) ok_begin(NULL); \ + int count = ok_count_inc() + ok_failed(); \ + LIBOK_PRINTF("not ok %d - " format, count, ##__VA_ARGS__); \ + \ + if (LIBOK_PRINTF_NEEDS_NEWLINE) { \ + LIBOK_PRINTF("\n"); \ + } \ + \ + LIBOK_PRINTF(" ---"); \ + if (LIBOK_PRINTF_NEEDS_NEWLINE) { \ + LIBOK_PRINTF("\n"); \ + } \ + \ + LIBOK_PRINTF(" severity: fail"); \ + if (LIBOK_PRINTF_NEEDS_NEWLINE) { \ + LIBOK_PRINTF("\n"); \ + } \ + \ + LIBOK_PRINTF(" stack: |-"); \ + if (LIBOK_PRINTF_NEEDS_NEWLINE) { \ + LIBOK_PRINTF("\n"); \ + } \ + \ + LIBOK_PRINTF(" at %s (%s:%d)", \ + __FUNCTION__, \ + __FILE__, \ + __LINE__ \ + ); \ + if (LIBOK_PRINTF_NEEDS_NEWLINE) { \ + LIBOK_PRINTF("\n"); \ + } \ + \ + LIBOK_PRINTF(" ..."); \ + if (LIBOK_PRINTF_NEEDS_NEWLINE) { \ + LIBOK_PRINTF("\n"); \ + } \ +}) + +/** + * Called at the beginning of a test with an optional (NULL) label. + */ +void ok_begin (const char *label); + +/** + * Can be used to printf a comment. + */ +void ok_comment (const char *comment); + +/** + * Can be used to provide a multiline test explaination. + */ +void ok_explain (const char *explaination); + +/** + * Can be used to issue an emergency "Bail out!" statement with an + * optional comment. + */ +void ok_bail (const char *comment); + +/** + * Completes tests and asserts that + * the expected test count matches the + * actual test count if the expected + * count is greater than 0 + */ +bool ok_done (void); + +/** + * Sets the expectation count + */ +void ok_expect (int); + +/** + * Returns the expected count + */ +int ok_expected (void); + +/** + * Returns the ok count + */ +int ok_count (void); +int ok_count_inc (void); + +/** + * Returns the not ok count + */ +int ok_failed (void); +int ok_failed_inc (void); + +/** + * Resets count and expected counters + */ +void ok_reset (void); + +#if defined(LIBOK_INCLUDE_IMPLEMENTATION) + +static int ok_count_; +static int ok_failed_; +static int ok_expected_; +static bool ok_begin_; +static bool ok_header_printed_; + +void ok_comment (const char *comment) { + LIBOK_PRINTF("# %s", comment); + if (LIBOK_PRINTF_NEEDS_NEWLINE) { + LIBOK_PRINTF("\n"); + } +} + +void ok_explain (const char *explaination) { + if (ok_count() == 0 && ok_failed() == 0) ok_begin(NULL); + ok_comment(""); + int i = 0; + while (explaination[i] != '\0') { + const char* pointer = explaination + i; + int j = i; + + while (explaination[i] != '\n' && explaination[i] != '0') { + i++; + } + + j = i - j; + LIBOK_PRINTF("# %.*s", j, pointer); + if (LIBOK_PRINTF_NEEDS_NEWLINE) { + LIBOK_PRINTF("\n"); + } + + i++; + } + ok_comment(""); +} + +void ok_bail (const char *comment) { + LIBOK_PRINTF("Bail out!"); + + if (comment != NULL) { + LIBOK_PRINTF(" %s", comment); + } + + if (LIBOK_PRINTF_NEEDS_NEWLINE) { + LIBOK_PRINTF("\n"); + } +} + +void ok_begin (const char *label) { + if (ok_begin_) return; + ok_begin_ = true; + + // print new line for new test + if (LIBOK_PRINTF_NEEDS_NEWLINE) { + LIBOK_PRINTF("\n"); + } else { + LIBOK_PRINTF(""); + } + + if (!ok_header_printed_) { + ok_header_printed_ = true; + LIBOK_PRINTF("TAP version 14"); + if (LIBOK_PRINTF_NEEDS_NEWLINE) { + LIBOK_PRINTF("\n"); + } + } + + if (label != NULL) { + LIBOK_PRINTF("# %s", label); + if (LIBOK_PRINTF_NEEDS_NEWLINE) { + LIBOK_PRINTF("\n"); + } + } +} + +bool ok_done (void) { + if (ok_count() == 0 && ok_failed() == 0) ok_begin(NULL); + + const int expected = ok_expected(); + const int failed = ok_failed(); + const int count = ok_count(); + bool success = true; + + if (0 != expected && count != expected) { + if (expected > count) { + LIBOK_FPRINTF(stderr, "expected number of success conditions not met."); + if (LIBOK_FPRINTF_NEEDS_NEWLINE) { + LIBOK_FPRINTF(stderr, "\n"); + } + } else { + LIBOK_FPRINTF(stderr, + "expected number of success conditions is less than the " + "number of given success conditions."); + if (LIBOK_FPRINTF_NEEDS_NEWLINE) { + LIBOK_FPRINTF(stderr, "\n"); + } + } + + success = false; + } + + if (LIBOK_PRINTF_NEEDS_NEWLINE) { + LIBOK_PRINTF("\n"); + } else { + LIBOK_PRINTF(""); + } + + if (expected == 0) { + LIBOK_PRINTF("1..%d", count + failed); + if (LIBOK_PRINTF_NEEDS_NEWLINE) { + LIBOK_PRINTF("\n"); + } + } + + LIBOK_PRINTF("# tests %d", count + failed); + if (LIBOK_PRINTF_NEEDS_NEWLINE) { + LIBOK_PRINTF("\n"); + } + + LIBOK_PRINTF("# pass %d", count); + if (LIBOK_PRINTF_NEEDS_NEWLINE) { + LIBOK_PRINTF("\n"); + } + + if (failed > 0) { + LIBOK_PRINTF("# fail %d", failed); + if (LIBOK_PRINTF_NEEDS_NEWLINE) { + LIBOK_PRINTF("\n"); + } + + success = false; + } + + if (LIBOK_PRINTF_NEEDS_NEWLINE) { + LIBOK_PRINTF("\n"); + } else { + LIBOK_PRINTF(""); + } + + return success; +} + +void ok_expect (int expected) { + if (ok_count() == 0 && ok_failed() == 0) ok_begin(NULL); + ok_expected_ = expected; + LIBOK_PRINTF("1..%d", expected); + if (LIBOK_PRINTF_NEEDS_NEWLINE) { + LIBOK_PRINTF("\n"); + } +} + +int ok_expected (void) { + return ok_expected_; +} + +int ok_count_inc (void) { + return ++ok_count_; +} + +int ok_count (void) { + return ok_count_; +} + +int ok_failed_inc (void) { + return ++ok_failed_; +} + +int ok_failed (void) { + return ok_failed_; +} + +void ok_reset (void) { + ok_begin_ = false; + ok_count_ = 0; + ok_failed_ = 0; + ok_expected_ = 0; +} + +#endif +#ifdef __cplusplus +} +#endif +#endif diff --git a/test/package.json b/test/package.json index ddd7f12346..56ccac039b 100644 --- a/test/package.json +++ b/test/package.json @@ -4,6 +4,7 @@ "scripts": { "test": "npm run test:desktop", "test:desktop": "node ./scripts/test-desktop.js", + "test:runtime-core": "ssc build --run --headless --only-build --test runtime-core/main.js", "test:android": "node ./scripts/test-android.js", "test:ios-simulator": "node ./scripts/test-ios-simulator.js", "test:android-emulator": "sh ./scripts/shell.sh ./scripts/test-android-emulator.sh", diff --git a/test/socket.ini b/test/socket.ini index 39ad340774..4fd2efc53a 100644 --- a/test/socket.ini +++ b/test/socket.ini @@ -18,6 +18,7 @@ env[] = SOCKET_DEBUG_IPC env[] = SOCKET_MODULE_PATH_PREFIX [build.extensions] +runtime-core-tests = src/runtime-core simple-ipc-ping = src/extensions/simple/ipc-ping.cc sqlite3 = src/extensions/sqlite3 diff --git a/test/src/runtime-core/codec.cc b/test/src/runtime-core/codec.cc new file mode 100644 index 0000000000..41f4d6c239 --- /dev/null +++ b/test/src/runtime-core/codec.cc @@ -0,0 +1,82 @@ +#include "tests.hh" +#include "src/core/codec.hh" + +namespace SSC::Tests { + void codec (const Harness& t) { + t.test("SSC::encodeURIComponent", [](auto t) { + const auto encoded = SSC::encodeURIComponent( + "a % encoded string with foo@bar.com, $100, & #tag" + ); + + t.assert(encodeURIComponent("").size() == 0, "Empty input returns empty string"); + t.assert(encoded.size() != 0, "encoded has size"); + t.equals( + encoded, + "a%20%25%20encoded%20string%20with%20foo%40bar%2Ecom%2C%20%24100%2C%20%26%20%23tag", + "encoded value is correct" + ); + }); + + t.test("SSC::decodeURIComponent", [](auto t) { + const auto decoded = SSC::decodeURIComponent( + "a%20%25%20encoded%20string%20with%20foo%40bar%2Ecom%2C%20%24100%2C%20%26%20%23tag" + ); + + t.assert(decodeURIComponent("").size() == 0, "Empty input returns empty string"); + t.assert(decoded.size() != 0, "decoded has size"); + t.equals( + decoded, + "a % encoded string with foo@bar.com, $100, & #tag", + "decoded value is correct" + ); + }); + + t.test("SSC::encodeHexString", [](auto t) { + t.equals( + SSC::encodeHexString("hello world"), + "68656C6C6F20776F726C64", + "encodes 'hello world'" + ); + + t.equals( + SSC::encodeHexString("#F"), + "2346", + "encodes '\u0023\u0046'" + ); + + t.equals( + SSC::encodeHexString("{\"foo\":\"bar\",\"biz\":{\"baz\":\"boop\"}}"), + "7B22666F6F223A22626172222C2262697A223A7B2262617A223A22626F6F70227D7D", + "encodes '{\"foo\":\"bar\",\"biz\":{\"baz\":\"boop\"}}'" + ); + }); + + t.test("SSC::decodeHexString", [](auto t) { + t.equals( + SSC::decodeHexString("68656C6C6F20776F726C64"), + "hello world", + "decodes '68656C6C6F20776F726C64'" + ); + + t.equals( + SSC::decodeHexString("2346"), + "#F", + "decodes '2346'" + ); + + t.equals( + SSC::decodeHexString("7B22666F6F223A22626172222C2262697A223A7B2262617A223A22626F6F70227D7D"), + "{\"foo\":\"bar\",\"biz\":{\"baz\":\"boop\"}}", + "decodes '7B22666F6F223A22626172222C2262697A223A7B2262617A223A22626F6F70227D7D'" + ); + }); + + t.test("SSC::decodeUTF8", [](auto t) { + t.comment("skip: TODO(@jwerle)"); + }); + + t.test("SSC::toBytes", [](auto t) { + t.comment("skip: TODO(@jwerle)"); + }); + } +} diff --git a/test/src/runtime-core/config.cc b/test/src/runtime-core/config.cc new file mode 100644 index 0000000000..93bb921439 --- /dev/null +++ b/test/src/runtime-core/config.cc @@ -0,0 +1,9 @@ +#include "./tests.hh" +#include "src/core/config.hh" + +namespace SSC::Tests { + void config (const Harness& t) { + t.test("SSC::Config", [](auto t) { + }); + } +} diff --git a/test/src/runtime-core/env.cc b/test/src/runtime-core/env.cc new file mode 100644 index 0000000000..9acc5cf85b --- /dev/null +++ b/test/src/runtime-core/env.cc @@ -0,0 +1,6 @@ +#include "tests.hh" + +namespace SSC::Tests { + void env (const Harness& t) { + } +} diff --git a/test/src/runtime-core/harness.cc b/test/src/runtime-core/harness.cc new file mode 100644 index 0000000000..094b2d182b --- /dev/null +++ b/test/src/runtime-core/harness.cc @@ -0,0 +1,137 @@ +#include "tests.hh" +#include "src/core/types.hh" + +namespace SSC::Tests { + static std::mutex mutex; + + Harness::Harness () { + mutex.unlock(); + } + + bool Harness::run (TestRunner runner) const { + return run(false, "", runner); + } + + bool Harness::run (bool isAsync, TestRunner runner) const { + return run(isAsync, "", runner); + } + + bool Harness::run (bool isAsync, const String& label, TestRunner runner) const { + if (label.size() > 0) { + mutex.lock(); + this->label(label); + ok_reset(); + } + + runner(*this); + + if (label.size() > 0 ) { + if (!isAsync) { + mutex.unlock(); + } + + if (ok_count() > 0 || ok_failed() > 0 || ok_expected() > 0) { + auto success = ok_done(); + ok_reset(); + return success; + } + } + + return true; + } + + void Harness::end () const { + mutex.unlock(); + } + + bool Harness::test (const String& label, bool isAsync, TestRunner scope) const { + return this->run(isAsync, label, scope); + } + + bool Harness::test (const String& label, TestRunner scope) const { + return this->run(false, label, scope); + } + + void Harness::comment (const String& comment) const { + ok_comment(comment.c_str()); + } + + void Harness::label (const String& label) const { + ok_begin(label.c_str()); + } + + void Harness::log (const String& message) const { + sapi_log(0, message.c_str()); + } + + bool Harness::assert (bool assertion, const String& message) const { + if (assertion) { + ok("%s", message.c_str()); + return true; + } else { + notok("assertion failed: %s", message.c_str()); + return false; + } + } + + bool Harness::assert (int64_t value, const String& message) const { + return assert(value != 0, message); + } + + bool Harness::assert (double value, const String& message) const { + return assert(value != 0.0, message); + } + + bool Harness::assert (void* value, const String& message) const { + return assert(value != 0, message); + } + + bool Harness::assert (const String& value, const String& message) const { + return assert(value.size() != 0, message); + } + + bool Harness::equals (const char* left, const char* right, const String& message) const { + return equals(String(left), String(right), message); + } + + bool Harness::equals (const String& left, const String& right, const String& message) const { + if (left == right) { + ok("'%s' equals '%s': %s", left.c_str(), right.c_str(), message.c_str()); + return true; + } else { + notok("'%s' does not equal '%s': %s", left.c_str(), right.c_str(), message.c_str()); + return false; + } + } + + bool Harness::equals (const int64_t left, const int64_t right, const String& message) const { + if (left == right) { + ok("%lld equals %lld: %s", left, right, message.c_str()); + return true; + } else { + notok("%lld does not equal %lld: %s", left, right, message.c_str()); + return false; + } + } + + bool Harness::equals (const double left, const double right, const String& message) const { + if (left == right) { + ok("%f equals %f: %s", left, right, message.c_str()); + return true; + } else { + notok("%f does not equal %f: %s", left, right, message.c_str()); + return false; + } + } + + bool Harness::throws (std::function<void()> fn, const String& message) const { + try { + fn(); + notok("does not throw exception: %s", message.c_str()); + return false; + } catch (std::exception e) { + ok("throws exception: %s", message.c_str()); + return true; + } + } +} diff --git a/test/src/runtime-core/ini.cc b/test/src/runtime-core/ini.cc new file mode 100644 index 0000000000..7a75a0f088 --- /dev/null +++ b/test/src/runtime-core/ini.cc @@ -0,0 +1,6 @@ +#include "tests.hh" + +namespace SSC::Tests { + void ini (const Harness& t) { + } +} diff --git a/test/src/runtime-core/json.cc b/test/src/runtime-core/json.cc new file mode 100644 index 0000000000..1b622fc164 --- /dev/null +++ b/test/src/runtime-core/json.cc @@ -0,0 +1,6 @@ +#include "tests.hh" + +namespace SSC::Tests { + void json (const Harness& t) { + } +} diff --git a/test/src/runtime-core/main.cc b/test/src/runtime-core/main.cc new file mode 100644 index 0000000000..43a9bfd522 --- /dev/null +++ b/test/src/runtime-core/main.cc @@ -0,0 +1,18 @@ +#include <socket/extension.h> +#include "tests.hh" + +bool initialize (sapi_context_t* context, const void *data) { + bool success = true; + SSC::Tests::Harness harness; + if (!harness.run(SSC::Tests::codec)) success = false; + if (!harness.run(SSC::Tests::config)) success = false; + if (!harness.run(SSC::Tests::env)) success = false; + if (!harness.run(SSC::Tests::ini)) success = false; + if (!harness.run(SSC::Tests::json)) success = false; + if (!harness.run(SSC::Tests::platform)) success = false; + if (!harness.run(SSC::Tests::preload)) success = false; + if (!harness.run(SSC::Tests::version)) success = false; + return success; +} + +SOCKET_RUNTIME_REGISTER_EXTENSION("runtime-core-tests", initialize); diff --git a/test/src/runtime-core/main.js b/test/src/runtime-core/main.js new file mode 100644 index 0000000000..f08122186f --- /dev/null +++ b/test/src/runtime-core/main.js @@ -0,0 +1,10 @@ +import extension from 'socket:extension' +import process from 'socket:process' + +try { + await extension.load('runtime-core-tests') + process.exit(0) +} catch (err) { + console.error(err.message || err) + process.exit(1) +} diff --git a/test/src/runtime-core/ok.cc b/test/src/runtime-core/ok.cc new file mode 100644 index 0000000000..18b196090b --- /dev/null +++ b/test/src/runtime-core/ok.cc @@ -0,0 +1,3 @@ +// inline `libok` implementation here for single unit +#define LIBOK_INCLUDE_IMPLEMENTATION +#include "./ok.hh" diff --git a/test/src/runtime-core/ok.hh b/test/src/runtime-core/ok.hh new file mode 100644 index 0000000000..0c0adc5136 --- /dev/null +++ b/test/src/runtime-core/ok.hh @@ -0,0 +1,11 @@ +#ifndef RUNTIME_CORE_TESTS_OK_H +#define RUNTIME_CORE_TESTS_OK_H + +#include <socket/extension.h> + +#define LIBOK_PRINTF_NEEDS_NEWLINE 0 +#define LIBOK_PRINTF(...) sapi_printf(0, __VA_ARGS__) +#define LIBOK_FPRINTF(unsued, ...) sapi_printf(0, __VA_ARGS__) + +#include "test/deps/ok/ok.h" +#endif diff --git a/test/src/runtime-core/platform.cc b/test/src/runtime-core/platform.cc new file mode 100644 index 0000000000..4de0114823 --- /dev/null +++ b/test/src/runtime-core/platform.cc @@ -0,0 +1,6 @@ +#include "tests.hh" + +namespace SSC::Tests { + void platform (const Harness& t) { + } +} diff --git a/test/src/runtime-core/preload.cc b/test/src/runtime-core/preload.cc new file mode 100644 index 0000000000..3255aa4707 --- /dev/null +++ b/test/src/runtime-core/preload.cc @@ -0,0 +1,6 @@ +#include "tests.hh" + +namespace SSC::Tests { + void preload (const Harness& t) { + } +} diff --git a/test/src/runtime-core/socket.ini b/test/src/runtime-core/socket.ini new file mode 100644 index 0000000000..aad94bc4f7 --- /dev/null +++ b/test/src/runtime-core/socket.ini @@ -0,0 +1,27 @@ +[meta] +name = "runtime-core-tests" +type = "extension" + +[extension] +# support files +sources[] = ../../deps/ok/ok.h +sources[] = ./harness.cc +sources[] = ./main.cc +sources[] = ./ok.cc + +# test files +sources[] = ./codec.cc +sources[] = ./config.cc +sources[] = ./env.cc +sources[] = ./ini.cc +sources[] = ./json.cc +sources[] = ./platform.cc +sources[] = ./preload.cc +sources[] = ./version.cc + +[extension.compiler] +flags[] = -fsanitize=undefined-trap +flags[] = -fsanitize-undefined-trap-on-error +flags[] = -ftrap-function=abort +flags[] = -DLIBOK_PRINTF_NEEDS_NEWLINE=0 +flags[] = -I../../.. diff --git a/test/src/runtime-core/tests.hh b/test/src/runtime-core/tests.hh new file mode 100644 index 0000000000..b0bca984ba --- /dev/null +++ b/test/src/runtime-core/tests.hh @@ -0,0 +1,51 @@ +#ifndef RUNTIME_CORE_TESTS_H +#define RUNTIME_CORE_TESTS_H + +#include <functional> + +#include <socket/extension.h> +#include "src/core/types.hh" +#include "./ok.hh" + +namespace SSC::Tests { + class Harness; + typedef void (TestRunner)(const Harness& harness); + + class Harness { + public: + Harness (); + + bool assert (bool assertion, const String& message = "") const; + bool assert (int64_t value, const String& message = "") const; + bool assert (double value, const String& message = "") const; + bool assert (void* value, const String& message = "") const; + bool assert (const String& value, const String& message) const; + bool equals (const char* left, const char* right, const String& message) const; + bool equals (const String& left, const String& right, const String& message) const; + bool equals (const int64_t left, const int64_t right, const String& message) const; + bool equals (const double left, const double right, const String& message) const; + bool throws (std::function<void()> fn, const String& message) const; + + void comment (const String& comment) const; + void label (const String& label) const; + bool test (const String& label, TestRunner scope) const; + bool test (const String& label, bool isAsync, TestRunner scope) const; + void log (const String& message) const; + bool run (bool isAsync, const String& label, TestRunner runner) const; + bool run (bool isAsync, TestRunner runner) const; + bool run (TestRunner) const; + void end () const; + }; + + // tests + void codec (const Harness&); + void config (const Harness&); + void env (const Harness&); + void ini (const Harness&); + void json (const Harness&); + void platform (const Harness&); + void preload (const Harness&); + void version (const Harness&); +} + +#endif diff --git a/test/src/runtime-core/version.cc b/test/src/runtime-core/version.cc new file mode 100644 index 0000000000..07d6cedfbc --- /dev/null +++ b/test/src/runtime-core/version.cc @@ -0,0 +1,6 @@ +#include "tests.hh" + +namespace SSC::Tests { + void version (const Harness& t) { + } +} From 0bd1a79beccae3584d6875ecb598dc881b69d2ce Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Fri, 13 Oct 2023 14:10:29 -0400 Subject: [PATCH 215/256] chore(clib): upgrade 'libok' --- test/clib.json | 2 +- test/deps/ok/clib.json | 2 +- test/deps/ok/ok.h | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/clib.json b/test/clib.json index 0e499b34cc..7bfeb12b54 100644 --- a/test/clib.json +++ b/test/clib.json @@ -3,6 +3,6 @@ "private": true, "src": [], "dependencies": { - "jwerle/libok": "0.6.3" + "jwerle/libok": "0.6.4" } } diff --git a/test/deps/ok/clib.json b/test/deps/ok/clib.json index 9be4876531..9f515527ea 100644 --- a/test/deps/ok/clib.json +++ b/test/deps/ok/clib.json @@ -1,6 +1,6 @@ { "name": "ok", - "version": "0.6.3", + "version": "0.6.4", "author": "Joseph Werle", "description": "Super tiny tap output library", "repo": "jwerle/libok", diff --git a/test/deps/ok/ok.h b/test/deps/ok/ok.h index a8b9886236..3e1b39457e 100644 --- a/test/deps/ok/ok.h +++ b/test/deps/ok/ok.h @@ -19,7 +19,7 @@ extern "C" { * libok version */ #ifndef OK_VERSION -#define OK_VERSION "0.6.3" +#define OK_VERSION "0.6.4" #endif /** @@ -76,7 +76,7 @@ extern "C" { */ #define notok(format, ...) ({ \ if (ok_count() == 0 && ok_failed() == 0) ok_begin(NULL); \ - int count = ok_count_inc() + ok_failed(); \ + int count = ok_count() + ok_failed_inc(); \ LIBOK_PRINTF("not ok %d - " format, count, ##__VA_ARGS__); \ \ if (LIBOK_PRINTF_NEEDS_NEWLINE) { \ From b6b84856b154f85599ad0ac8d88f65e9a8e3b5ef Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Fri, 13 Oct 2023 18:05:59 -0400 Subject: [PATCH 216/256] fix(src/cli/cli.cc): fix exit signal status on macos --- src/cli/cli.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index a39996be05..7fe3727f77 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -448,7 +448,9 @@ void pollOSLogStream (bool isForDesktop, String bundleIdentifier, int processIde // have access to, filter them out if (String(message) != "<private>") { if (String(message).starts_with("__EXIT_SIGNAL__")) { - appStatus = std::stoi(replace(String(message), "__EXIT_SIGNAL__=", "")); + if (appStatus == -1) { + appStatus = std::stoi(replace(String(message), "__EXIT_SIGNAL__=", "")); + } } else if ( entry.level == OSLogEntryLogLevelDebug || entry.level == OSLogEntryLogLevelError || From b1ab853ee1cbba3d09ee88d2b2e249968e873db3 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Fri, 13 Oct 2023 18:06:27 -0400 Subject: [PATCH 217/256] refactor(src/core/platform.{cc,hh}): support 'platform.android' --- src/core/platform.cc | 1 + src/core/platform.hh | 1 + 2 files changed, 2 insertions(+) diff --git a/src/core/platform.cc b/src/core/platform.cc index fff2f167f4..b9c774f245 100644 --- a/src/core/platform.cc +++ b/src/core/platform.cc @@ -43,6 +43,7 @@ namespace SSC { .os = "linux", #endif + .android = true, .linux = true, #if defined(__unix__) || defined(unix) || defined(__unix) diff --git a/src/core/platform.hh b/src/core/platform.hh index 96b4dfb202..6de1c906ac 100644 --- a/src/core/platform.hh +++ b/src/core/platform.hh @@ -111,6 +111,7 @@ namespace SSC { bool mac = false; bool ios = false; bool win = false; + bool android = false; bool linux = false; bool unix = false; }; From bf888cb449a9401b9f6b6ab2c5e4a9d31ce92f12 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Fri, 13 Oct 2023 18:06:47 -0400 Subject: [PATCH 218/256] refactor(src/core/types.hh): introduce 'Atomic<T>' type alias --- src/core/types.hh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/types.hh b/src/core/types.hh index 79a63609ed..f9c5912434 100644 --- a/src/core/types.hh +++ b/src/core/types.hh @@ -2,6 +2,7 @@ #define SSC_CORE_TYPES_H #include <array> +#include <atomic> #include <filesystem> #include <functional> #include <map> @@ -32,6 +33,7 @@ namespace SSC { using Lock = std::lock_guard<Mutex>; using Thread = std::thread; + template <typename T> using Atomic = std::atomic<T>; template <typename T, int k> using Array = std::array<T, k>; template <typename T> using Queue = std::queue<T>; template <typename T> using Vector = std::vector<T>; From ba767cf4fc5d5bdff75f0d60afd998d8d48d569d Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Fri, 13 Oct 2023 18:10:40 -0400 Subject: [PATCH 219/256] chore(clib): upgrade 'libok' --- test/clib.json | 2 +- test/deps/ok/clib.json | 2 +- test/deps/ok/ok.h | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/clib.json b/test/clib.json index 7bfeb12b54..4a5f1d9db6 100644 --- a/test/clib.json +++ b/test/clib.json @@ -3,6 +3,6 @@ "private": true, "src": [], "dependencies": { - "jwerle/libok": "0.6.4" + "jwerle/libok": "0.6.5" } } diff --git a/test/deps/ok/clib.json b/test/deps/ok/clib.json index 9f515527ea..c4bffc2e73 100644 --- a/test/deps/ok/clib.json +++ b/test/deps/ok/clib.json @@ -1,6 +1,6 @@ { "name": "ok", - "version": "0.6.4", + "version": "0.6.5", "author": "Joseph Werle", "description": "Super tiny tap output library", "repo": "jwerle/libok", diff --git a/test/deps/ok/ok.h b/test/deps/ok/ok.h index 3e1b39457e..8cfb022c2b 100644 --- a/test/deps/ok/ok.h +++ b/test/deps/ok/ok.h @@ -19,7 +19,7 @@ extern "C" { * libok version */ #ifndef OK_VERSION -#define OK_VERSION "0.6.4" +#define OK_VERSION "0.6.5" #endif /** @@ -98,7 +98,7 @@ extern "C" { LIBOK_PRINTF("\n"); \ } \ \ - LIBOK_PRINTF(" at %s (%s:%d)", \ + LIBOK_PRINTF(" at %s (%s:%d)", \ __FUNCTION__, \ __FILE__, \ __LINE__ \ From e1a3e6dfb6f75321e18eb8abc0c9b72dc1fc4481 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Fri, 13 Oct 2023 18:10:51 -0400 Subject: [PATCH 220/256] test(runtime-core): more tests --- test/src/runtime-core/harness.cc | 65 +++++++++++++++++++++-- test/src/runtime-core/main.cc | 22 ++++---- test/src/runtime-core/main.js | 11 ++-- test/src/runtime-core/platform.cc | 86 +++++++++++++++++++++++++++++++ test/src/runtime-core/tests.hh | 22 ++++++-- 5 files changed, 185 insertions(+), 21 deletions(-) diff --git a/test/src/runtime-core/harness.cc b/test/src/runtime-core/harness.cc index 094b2d182b..f358798a9f 100644 --- a/test/src/runtime-core/harness.cc +++ b/test/src/runtime-core/harness.cc @@ -1,13 +1,19 @@ #include "tests.hh" #include "src/core/types.hh" +#include "./ok.hh" namespace SSC::Tests { - static std::mutex mutex; + static Harness::Mutex mutex; + static Atomic<int> pending = 0; Harness::Harness () { mutex.unlock(); } + Harness::Harness (const Options& options) : options(options) { + mutex.unlock(); + } + bool Harness::run (TestRunner runner) const { return run(false, "", runner); } @@ -20,16 +26,22 @@ namespace SSC::Tests { if (label.size() > 0) { mutex.lock(); this->label(label); - ok_reset(); + if (this->options.resetContextAfterEachRun) { + ok_reset(); + } } + pending++; runner(*this); + pending--; if (label.size() > 0 ) { if (!isAsync) { mutex.unlock(); } + } + if (pending == 0) { if (ok_count() > 0 || ok_failed() > 0 || ok_expected() > 0) { auto success = ok_done(); ok_reset(); @@ -37,7 +49,7 @@ namespace SSC::Tests { } } - return true; + return false; } void Harness::end () const { @@ -57,7 +69,8 @@ namespace SSC::Tests { } void Harness::label (const String& label) const { - ok_begin(label.c_str()); + ok_begin(nullptr); + ok_comment(label.c_str()); } void Harness::log (const String& message) const { @@ -104,6 +117,16 @@ namespace SSC::Tests { } } + bool Harness::equals (const bool left, const bool right, const String& message) const { + if (left == right) { + ok("%s equals %s: %s", left ? "true" : "false", right ? "true" : "false", message.c_str()); + return true; + } else { + notok("%s does not equal %s: %s", left ? "true" : "false", right ? "true" : "false", message.c_str()); + return false; + } + } + bool Harness::equals (const int64_t left, const int64_t right, const String& message) const { if (left == right) { ok("%lld equals %lld: %s", left, right, message.c_str()); @@ -124,6 +147,40 @@ namespace SSC::Tests { } } + bool Harness::notEquals (const String& left, const String& right, const String& message) const { + if (left == right) { + notok("'%s' equals '%s': %s", left.c_str(), right.c_str(), message.c_str()); + return false; + } else { + ok("'%s' does not equal '%s': %s", left.c_str(), right.c_str(), message.c_str()); + return true; + } + } + + bool Harness::notEquals (const char* left, const char* right, const String& message) const { + return notEquals(String(left), String(right), message); + } + + bool Harness::notEquals (const int64_t left, const int64_t right, const String& message) const { + if (left == right) { + notok("%lld equals %lld: %s", left, right, message.c_str()); + return false; + } else { + ok("%lld does not equal %lld: %s", left, right, message.c_str()); + return true; + } + } + + bool Harness::notEquals (const double left, const double right, const String& message) const { + if (left == right) { + notok("%f equals %f: %s", left, right, message.c_str()); + return false; + } else { + ok("%f does not equal %f: %s", left, right, message.c_str()); + return true; + } + } + bool Harness::throws (std::function<void()> fn, const String& message) const { try { fn(); diff --git a/test/src/runtime-core/main.cc b/test/src/runtime-core/main.cc index 43a9bfd522..36979b74c8 100644 --- a/test/src/runtime-core/main.cc +++ b/test/src/runtime-core/main.cc @@ -1,18 +1,18 @@ #include <socket/extension.h> #include "tests.hh" -bool initialize (sapi_context_t* context, const void *data) { - bool success = true; +static bool initialize (sapi_context_t* context, const void *data) { SSC::Tests::Harness harness; - if (!harness.run(SSC::Tests::codec)) success = false; - if (!harness.run(SSC::Tests::config)) success = false; - if (!harness.run(SSC::Tests::env)) success = false; - if (!harness.run(SSC::Tests::ini)) success = false; - if (!harness.run(SSC::Tests::json)) success = false; - if (!harness.run(SSC::Tests::platform)) success = false; - if (!harness.run(SSC::Tests::preload)) success = false; - if (!harness.run(SSC::Tests::version)) success = false; - return success; + return harness.run("runtime-core-tests", [](auto t) { + t.run(SSC::Tests::codec); + t.run(SSC::Tests::config); + t.run(SSC::Tests::env); + t.run(SSC::Tests::ini); + t.run(SSC::Tests::json); + t.run(SSC::Tests::platform); + t.run(SSC::Tests::preload); + t.run(SSC::Tests::version); + }); } SOCKET_RUNTIME_REGISTER_EXTENSION("runtime-core-tests", initialize); diff --git a/test/src/runtime-core/main.js b/test/src/runtime-core/main.js index f08122186f..91df1a9181 100644 --- a/test/src/runtime-core/main.js +++ b/test/src/runtime-core/main.js @@ -1,10 +1,15 @@ import extension from 'socket:extension' import process from 'socket:process' +const EXIT_TIMEOUT = 250 + try { await extension.load('runtime-core-tests') - process.exit(0) + setTimeout(() => process.exit(1), EXIT_TIMEOUT) } catch (err) { - console.error(err.message || err) - process.exit(1) + if (!/failed to load/i.test(err?.message)) { + console.error(err.message || err) + } + + setTimeout(() => process.exit(1), EXIT_TIMEOUT) } diff --git a/test/src/runtime-core/platform.cc b/test/src/runtime-core/platform.cc index 4de0114823..7d0fb2b963 100644 --- a/test/src/runtime-core/platform.cc +++ b/test/src/runtime-core/platform.cc @@ -1,6 +1,92 @@ #include "tests.hh" +#include "src/core/platform.hh" namespace SSC::Tests { void platform (const Harness& t) { + t.test("SSC::platform.arch", [](auto t) { + t.assert(SSC::platform.arch, "SSC::platform.arch is not empty"); + #if defined(__x86_64__) || defined(_M_X64) + t.equals(SSC::platform.arch, "x86_64", "SSC::platform.arch == \"x86_64\""); + #elif defined(__aarch64__) || defined(_M_ARM64) + t.equals(SSC::platform.arch , "arm64", "SSC::platform.arch == \"arm64\""); + #else + t.equals(SSC::platform.arch , "unknown", "SSC::platform.arch == \"unknown\""); + #endif + }); + + t.test("SSC::platform.os", [](auto t) { + t.assert(SSC::platform.os, "SSC::platform.osis not empty"); + #if defined(_WIN32) + t.equals(SSC::platform.os, "win32", "SSC::platform.os == \"win32\""); + #elif defined(__APPLE__) + #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + t.equals(SSC::platform.os, "ios", "SSC::platform.os == \"ios\""); + #else + t.equals(SSC::platform.os, "mac", "SSC::platform.os == \"mac\""); + #endif + #elif defined(__ANDROID__) + t.equals(SSC::platform.os, "android", "SSC::platform.os == \"android\""); + #elif defined(__linux__) + t.equals(SSC::platform.os, "linux", "SSC::platform.os == \"linux\""); + #elif defined(__FreeBSD__) + t.equals(SSC::platform.os, "freebsd", "SSC::platform.os == \"freebsd\""); + #elif defined(BSD) + t.equals(SSC::platform.os, "openbsd", "SSC::platform.os == \"openbsd\""); + #endif + }); + + t.test("SSC::platform.{mac,ios,win,linux,unix}", [](auto t) { + #if defined(_WIN32) + t.equals(SSC::platform.mac, false, "SSC::platform.mac = false"); + t.equals(SSC::platform.ios, false, "SSC::platform.ios = false"); + t.equals(SSC::platform.win, true, "SSC::platform.win = true"); + t.equals(SSC::platform.linux, false, "SSC::platform.linux = false"); + t.equals(SSC::platform.android, false, "SSC::platform.android = false"); + #elif defined(__APPLE__) + #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + t.equals(SSC::platform.mac, false, "SSC::platform.mac = false"); + t.equals(SSC::platform.ios, true, "SSC::platform.ios = true"); + t.equals(SSC::platform.win, false, "SSC::platform.win = false"); + t.equals(SSC::platform.linux, false, "SSC::platform.linux = false"); + t.equals(SSC::platform.android, false, "SSC::platform.android = false"); + #else + t.equals(SSC::platform.mac, true, "SSC::platform.mac = true"); + t.equals(SSC::platform.ios, false, "SSC::platform.ios = false"); + t.equals(SSC::platform.win, false, "SSC::platform.win = false"); + t.equals(SSC::platform.linux, false, "SSC::platform.linux = false"); + t.equals(SSC::platform.android, false, "SSC::platform.android = false"); + #endif + #elif defined(__ANDROID__) + t.equals(SSC::platform.mac, false, "SSC::platform.mac = false"); + t.equals(SSC::platform.ios, false, "SSC::platform.ios = false"); + t.equals(SSC::platform.win, false, "SSC::platform.win = false"); + t.equals(SSC::platform.linux, true, "SSC::platform.linux = true"); + t.equals(SSC::platform.android, true, "SSC::platform.android = true"); + #elif defined(__linux__) + t.equals(SSC::platform.mac, false, "SSC::platform.mac = false"); + t.equals(SSC::platform.ios, false, "SSC::platform.ios = false"); + t.equals(SSC::platform.win, false, "SSC::platform.win = false"); + t.equals(SSC::platform.linux, true, "SSC::platform.linux = true"); + t.equals(SSC::platform.android, false, "SSC::platform.android = false"); + #elif defined(__FreeBSD__) + t.equals(SSC::platform.mac, false, "SSC::platform.mac = false"); + t.equals(SSC::platform.ios, false, "SSC::platform.ios = false"); + t.equals(SSC::platform.win, false, "SSC::platform.win = false"); + t.equals(SSC::platform.linux, false, "SSC::platform.linux = false"); + t.equals(SSC::platform.android, false, "SSC::platform.android = false"); + #elif defined(BSD) + t.equals(SSC::platform.mac, false, "SSC::platform.mac = false"); + t.equals(SSC::platform.ios, false, "SSC::platform.ios = false"); + t.equals(SSC::platform.win, false, "SSC::platform.win = false"); + t.equals(SSC::platform.linux, false, "SSC::platform.linux = false"); + t.equals(SSC::platform.android, false, "SSC::platform.android = false"); + #endif + + #if defined(__unix__) || defined(unix) || defined(__unix) + t.equals(SSC::platform.unix, true, "SSC::platform.unix = true"); + #else + t.equals(SSC::platform.unix, false, "SSC::platform.unix = false"); + #endif + }); } } diff --git a/test/src/runtime-core/tests.hh b/test/src/runtime-core/tests.hh index b0bca984ba..d141644cd6 100644 --- a/test/src/runtime-core/tests.hh +++ b/test/src/runtime-core/tests.hh @@ -4,8 +4,9 @@ #include <functional> #include <socket/extension.h> -#include "src/core/types.hh" -#include "./ok.hh" +#include "src/core/core.hh" + +#undef assert namespace SSC::Tests { class Harness; @@ -13,17 +14,32 @@ namespace SSC::Tests { class Harness { public: + struct Options { + bool resetContextAfterEachRun = false; + }; + + using Mutex = std::mutex; + const Options options; Harness (); + Harness (const Options& options); bool assert (bool assertion, const String& message = "") const; bool assert (int64_t value, const String& message = "") const; bool assert (double value, const String& message = "") const; bool assert (void* value, const String& message = "") const; bool assert (const String& value, const String& message) const; - bool equals (const char* left, const char* right, const String& message) const; + bool equals (const String& left, const String& right, const String& message) const; + bool equals (const char* left, const char* right, const String& message) const; + bool equals (const bool left, const bool right, const String& message) const; bool equals (const int64_t left, const int64_t right, const String& message) const; bool equals (const double left, const double right, const String& message) const; + + bool notEquals (const String& left, const String& right, const String& message) const; + bool notEquals (const char* left, const char* right, const String& message) const; + bool notEquals (const int64_t left, const int64_t right, const String& message) const; + bool notEquals (const double left, const double right, const String& message) const; + bool throws (std::function<void()> fn, const String& message) const; void comment (const String& comment) const; From 4b098acb304f4cf19a0b4a3b670834c5070a0a9d Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Fri, 13 Oct 2023 18:52:00 -0400 Subject: [PATCH 221/256] fix(src/core/ini.cc): fix separator usage --- src/core/ini.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ini.cc b/src/core/ini.cc index 8c04e6eb2b..c9ea28b28f 100644 --- a/src/core/ini.cc +++ b/src/core/ini.cc @@ -25,7 +25,7 @@ namespace SSC::INI { prefix = entry.substr(1, entry.length() - 2); } - prefix = replace(prefix, "\\.", "_"); + prefix = replace(prefix, "\\.", keyPathSeparator); if (prefix.size() > 0) { prefix += keyPathSeparator; } From 2c1f8e7f2f2658ecd18f85c4dad44bdfa0452137 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Fri, 13 Oct 2023 18:52:47 -0400 Subject: [PATCH 222/256] test(runtime-core/ini): 'INI::parse' tests --- test/src/runtime-core/ini.cc | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/test/src/runtime-core/ini.cc b/test/src/runtime-core/ini.cc index 7a75a0f088..75f49b8708 100644 --- a/test/src/runtime-core/ini.cc +++ b/test/src/runtime-core/ini.cc @@ -2,5 +2,78 @@ namespace SSC::Tests { void ini (const Harness& t) { + t.test("SSC::INI::parse", [] (auto t) { + auto simple = SSC::INI::parse(R"INI( + key = "value" + )INI"); + + t.equals(simple["key"], "value", "simple[key] == value"); + + auto sections = SSC::INI::parse(R"INI( + [section-1] + key = "value" + + [section-2] + key = "value" + + [section-3] + key = "value" + )INI"); + + t.equals(sections["section-1_key"], "value", "sections[section-1_key] == value"); + t.equals(sections["section-2_key"], "value", "sections[section-2_key] == value"); + t.equals(sections["section-3_key"], "value", "sections[section-3_key] == value"); + + auto subsections = SSC::INI::parse(R"INI( + [section-1] + key = "value" + [.subsection] + key = "value" + + [section-2] + key = "value" + [.subsection] + key = "value" + + [section-3] + key = "value" + [.subsection] + key = "value" + )INI"); + + t.equals(subsections["section-1_key"], "value", "subsections[section-1_key] == value"); + t.equals(subsections["section-2_key"], "value", "subsections[section-2_key] == value"); + t.equals(subsections["section-3_key"], "value", "subsections[section-3_key] == value"); + + t.equals(subsections["section-1_subsection_key"], "value", "subsections[section-1_subsection_key] == value"); + t.equals(subsections["section-2_subsection_key"], "value", "subsections[section-2_subsection_key] == value"); + t.equals(subsections["section-3_subsection_key"], "value", "subsections[section-3_subsection_key] == value"); + + auto arrays = SSC::INI::parse(R"INI( + [numbers] + array[] = 1 + array[] = 2 + array[] = 3 + + [strings] + array[] = "hello" + array[] = "world" + )INI"); + + t.equals(arrays["numbers_array"], "1 2 3", "arrays[numbers_array] == 1 2 3"); + t.equals(arrays["strings_array"], "hello world", "arrays[strings_array] == hello world"); + + auto dotsyntax = SSC::INI::parse(R"INI( + [a.b.c.d.e.f] + g = "value" + + [a.b.c.d.e] + [.f.g.h] + i = "value" + )INI", "."); + + t.equals(dotsyntax["a.b.c.d.e.f.g"], "value", "dotsyntax[a.b.c.d.e.f.g] == value"); + t.equals(dotsyntax["a.b.c.d.e.f.g.h.i"], "value", "dotsyntax[a.b.c.d.e.f.g.h.i] == value"); + }); } } From 09528a431d671fbd1577ca45860492c04e6d261e Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Fri, 13 Oct 2023 18:52:59 -0400 Subject: [PATCH 223/256] fix(test/src/runtime-core/main.js): fix wrong exit code --- test/src/runtime-core/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/runtime-core/main.js b/test/src/runtime-core/main.js index 91df1a9181..6c303808d3 100644 --- a/test/src/runtime-core/main.js +++ b/test/src/runtime-core/main.js @@ -5,7 +5,7 @@ const EXIT_TIMEOUT = 250 try { await extension.load('runtime-core-tests') - setTimeout(() => process.exit(1), EXIT_TIMEOUT) + setTimeout(() => process.exit(0), EXIT_TIMEOUT) } catch (err) { if (!/failed to load/i.test(err?.message)) { console.error(err.message || err) From daacf971817078421a375a3c74fb6c3ef78ac64a Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 18 Oct 2023 10:55:57 -0400 Subject: [PATCH 224/256] refactor(bin/cflags.sh): conditionally set 'ios/ios-simulator' min version --- bin/cflags.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bin/cflags.sh b/bin/cflags.sh index 9dc0ea2034..2c054a8951 100755 --- a/bin/cflags.sh +++ b/bin/cflags.sh @@ -65,11 +65,15 @@ if (( TARGET_OS_IPHONE )) || (( TARGET_IPHONE_SIMULATOR )); then cflags+=("-arch arm64") cflags+=("-target arm64-apple-ios") cflags+=("-Wno-unguarded-availability-new") - cflags+=("-miphoneos-version-min=$IPHONEOS_VERSION_MIN") + if [ -n "$IPHONEOS_VERSION_MIN" ]; then + cflags+=("-miphoneos-version-min=$IPHONEOS_VERSION_MIN") + fi elif (( TARGET_IPHONE_SIMULATOR )); then ios_sdk_path="$(xcrun -sdk iphonesimulator -show-sdk-path)" cflags+=("-arch $arch") - cflags+=("-mios-simulator-version-min=$IPHONEOS_VERSION_MIN") + if [ -n "$IOS_SIMULATOR_VERSION_MIN" ]; then + cflags+=("-mios-simulator-version-min=$IOS_SIMULATOR_VERSION_MIN") + fi fi cflags+=("-iframeworkwithsysroot /System/Library/Frameworks") From 55fa2e73f05a6bb3971f260b7a7de9b2f6aa7f3e Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 18 Oct 2023 10:56:20 -0400 Subject: [PATCH 225/256] refactor(src/cli/cli.cc): increase log polls after exit --- src/cli/cli.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 7fe3727f77..2e55054ecb 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -333,7 +333,7 @@ void pollOSLogStream (bool isForDesktop, String bundleIdentifier, int processIde NSDate* latest = nil; NSError* error = nil; - int pollsAfterTermination = 4; + int pollsAfterTermination = 16; int backoffIndex = 0; // lucas series of backoffs From 19dcf89d684b4e402298e6673e8fa257a3948d28 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 18 Oct 2023 10:56:54 -0400 Subject: [PATCH 226/256] refactor(core/config): more accessor methods --- src/core/config.cc | 256 +++++++++++++++++++++++++++++++++++++++++---- src/core/config.hh | 87 ++++++++++++--- 2 files changed, 308 insertions(+), 35 deletions(-) diff --git a/src/core/config.cc b/src/core/config.cc index 57ff1eaba4..79e0f3ef3b 100644 --- a/src/core/config.cc +++ b/src/core/config.cc @@ -2,14 +2,17 @@ #include "ini.hh" #include "string.hh" +#include "debug.hh" + namespace SSC { - static const String NAMESPACE_SEPARATOR = "_"; + static constexpr char NAMESPACE_SEPARATOR = '.'; + static const String NAMESPACE_SEPARATOR_STRING = String(1, NAMESPACE_SEPARATOR); Config::Config (const String& source) { - this->map = INI::parse(source); + this->map = INI::parse(source, NAMESPACE_SEPARATOR_STRING); } - Config::Config (const Config& source) { + Config::Config (const Config& source) : prefix(source.prefix) { this->map = source.data(); } @@ -17,7 +20,15 @@ namespace SSC { this->map = source; } - const String Config::get (const String& key) const { + Config::Config (const String& prefix, const Map& source) : prefix(prefix) { + this->map = source; + } + + Config::Config (const String& prefix, const Config& source) : prefix(prefix) { + this->map = source.data(); + } + + const String Config::get (const String& key) const noexcept { if (this->contains(key)) { return this->at(key); } @@ -29,44 +40,50 @@ namespace SSC { return this->map.at(key); } - void Config::set (const String& key, const String& value) { + void Config::set (const String& key, const String& value) noexcept { this->map.insert_or_assign(key, value); } - bool Config::contains (const String& key) const { + const std::size_t Config::size () const noexcept { + return this->map.size(); + } + + bool Config::contains (const String& key) const noexcept { if (this->map.contains(key) && this->map.at(key).size() > 0) { return true; } - for (const auto& tuple : this->map) { - if ( - tuple.first.starts_with(key + NAMESPACE_SEPARATOR) && - tuple.second.size() > 0 - ) { - return true; - } - } - - return false; + return this->query(key).size() > 0; } - bool Config::erase (const String& key) { + bool Config::erase (const String& key) noexcept { if (this->map.contains(key)) { this->map.erase(key); return true; } - return false; + const auto view = this->query(key); + bool erased = false; + + for (const auto& tuple : view) { + if (this->map.contains(tuple.first)) { + this->map.erase(tuple.first); + erased = true; + } + } + + return erased; } - const Map& Config::data () const { + const Map& Config::data () const noexcept { return this->map; } - const Config Config::slice (const String& key) const { + const Config Config::slice (const String& key) const noexcept { + const auto view = this->query("[" + key + "]"); Map slice; - for (const auto& tuple : this->map) { + for (const auto& tuple : view) { if ( tuple.first.starts_with(key + NAMESPACE_SEPARATOR) && tuple.second.size() > 0 @@ -77,7 +94,171 @@ namespace SSC { } } - return Config { slice }; + return Config { key, slice }; + } + + const Config Config::query (const String& input) const noexcept { + struct State { + Vector<String> paths; + Vector<String> targets; + String property; + String compared; + String token; + bool parsingSingleQuote = false; + bool parsingDoubleQuote = false; + bool parsingNamespace = false; + bool parsingProperty = false; + bool negate = false; + bool compare = false; + }; + + String query = trim(input); + State state; + Map results; + + if (!query.starts_with("[") && !query.starts_with(NAMESPACE_SEPARATOR_STRING)) { + query = "[" + query + "]"; + } + + if (query.starts_with(NAMESPACE_SEPARATOR_STRING)) { + query = "[*]" + query; + } + + for (int i = 0; i < query.size(); ++i) { + const auto ch = query[i]; + + if (ch == '[') { + if (state.parsingNamespace) { + return Config {}; // error + } + + state.parsingNamespace = true; + state.token = ""; + } else if (ch == ']') { + if (!state.parsingNamespace) { + return Config {}; // error + } + + state.parsingNamespace = false; + if (state.token.size() == 0) { + // [] implies [*] + state.paths.push_back("*"); + } else { + state.paths.push_back(state.token); + state.token = ""; + } + } else if (state.parsingNamespace) { + state.token += ch; + } else if (ch == '.') { + state.parsingProperty = true; + } else if (state.parsingProperty) { + if (ch == ' ' && state.token.size() == 0) { + continue; + } else if (ch == '"') { + if (!state.parsingSingleQuote) { + state.parsingDoubleQuote = state.token.size() == 0; + continue; + } + } else if (ch == '\'') { + if (!state.parsingDoubleQuote) { + state.parsingSingleQuote = state.token.size() == 0; + continue; + } + } else if (ch == '!' && !state.parsingDoubleQuote && !state.parsingSingleQuote) { + if (query[i + 1] == '=') { + state.negate = true; + state.compare = true; + continue; + } else { + return Config {}; // error + } + } else if (ch == '=' && !state.parsingDoubleQuote && !state.parsingSingleQuote) { + state.compare = true; + continue; + } + + if (state.compare) { + state.compared += ch; + } else { + state.token += ch; + } + } + } + + if (state.parsingProperty && state.token.size()) { + state.property = trim(state.token); + state.token = ""; + } + + state.compared = trim(state.compared); + + const auto& path = join(state.paths, NAMESPACE_SEPARATOR_STRING); + for (const auto& tuple : this->map) { + const auto parts = split(tuple.first, NAMESPACE_SEPARATOR_STRING); + const auto& target = tuple.first; + const auto prefix = join( + Vector<String>(parts.begin(), parts.begin() + parts.size() - 1), + NAMESPACE_SEPARATOR_STRING + ); + + bool match = false; + if (path.starts_with(NAMESPACE_SEPARATOR_STRING)) { + if (state.compare) { + match = prefix.ends_with(path); + } else { + match = prefix.find(path) != String::npos; + } + } else if (path == "*") { + match = true; + } else if (prefix.starts_with(path)) { + match = true; + } + + if (match) { + if (state.property == "*") { + state.targets.push_back(target); + state.compare = false; + } else if (state.compare || state.property.size() > 0) { + state.targets.push_back(prefix); + } else { + state.targets.push_back(target); + } + } + } + + for (const auto& target : state.targets) { + const auto key = state.compare || state.property.size() > 0 + ? target + NAMESPACE_SEPARATOR_STRING + state.property + : target; + + if (!this->map.contains(key)) { + continue; + } + + const auto& value = this->map.at(key); + + if (state.compare) { + if (state.negate) { + if (value != state.compared) { + results[key] = value; + } + } else if (value == state.compared) { + results[key] = value; + } + } else { + results[key] = value; + } + } + + return Config { results }; + } + + const Vector<String> Config::keys () const noexcept { + Vector<String> results; + for (const auto& tuple : this->map) { + results.push_back(tuple.first); + } + return results; } const String Config::operator [] (const String& key) const { @@ -87,4 +268,35 @@ namespace SSC { const String& Config::operator [] (const String& key) { return this->map[key]; } + + const Config::Iterator Config::begin () const noexcept { + return this->map.begin(); + } + + const Config::Iterator Config::end () const noexcept { + return this->map.end(); + } + + const bool Config::clear () noexcept { + if (this->map.size() == 0) { + return false; + } + + this->map.clear(); + return true; + } + + const Vector<Config> Config::children () const noexcept { + Vector<Config> children; + Vector<String> seen; + for (const auto& tuple : this->map) { + const auto parts = split(tuple.first, NAMESPACE_SEPARATOR_STRING); + const auto duplicate = std::find(seen.begin(), seen.end(), parts[0]) != seen.end(); + if (parts.size() > 1 && !duplicate) { + seen.push_back(parts[0]); + children.push_back(Config(parts[0], this->slice(parts[0]))); + } + } + return children; + } } diff --git a/src/core/config.hh b/src/core/config.hh index 7a2ae81884..97724d0117 100644 --- a/src/core/config.hh +++ b/src/core/config.hh @@ -57,25 +57,35 @@ namespace SSC { */ Map map; public: - using Iterator = Map::iterator; + using Iterator = Map::const_iterator; + using Path = Vector<String>; + + /** + * The configuration prefix. + */ + const String prefix; /** * `Config` class constructors. */ Config () = default; - Config (const Map& source); Config (const String& source); + Config (const Map& source); Config (const Config& source); + Config (const String& prefix, const Map& source); + Config (const String& prefix, const Config& source); /** * Get a configuration value by name or `.` path. + * * @param key The configuration name or key path * @return The value at `key` or an empty string. */ - const String get (const String& key) const; + const String get (const String& key) const noexcept; /** * Get a configuration value reference by name. + * * @param key The configuration name or key path * @return `String&` The reference at `key` or an empty string. */ @@ -83,46 +93,74 @@ namespace SSC { /** * Set a configuration `value` by `key`. + * * @param key The configuration name of `value` to set * @param value The value of `key` to set */ - void set (const String& key, const String& value); + void set (const String& key, const String& value) noexcept; /** * Returns `true` if `key` exists in configuration and is not empty. + * * @param key The key to check for existence. * @return `true` if it exists, otherwise `false` */ - bool contains (const String& key) const; + bool contains (const String& key) const noexcept; /** * Erase a configuration value by `key`. - * @param key The key to erase a value for.. + * + * @param key The key to erase a value for. Can be valid input for `query` * @return `true` if erased, otherwise `false` */ - bool erase (const String& key); + bool erase (const String& key) noexcept; /** * Get a const reference to the underlying data map * of this configuration. + * * @return `Map&` A reference to the internal data map */ - const Map& data () const; + const Map& data () const noexcept; /** * Get a `Config` instance as a "slice" of this configuration, such as - * a subsection using `.` syntax or configuration section prefixes. + * a subsection using `.` syntax or configuration section prefixes. The + * returned `Config` instance is read-only. + * * @param key The key to filter on - * @return The value at `key` + * @return A `const Config` with section starting or matching `key` + * * @example * const auto config = Config::getUserConfig(); * const auto build = config.slice("build"); * const auto script = build["script"]; */ - const Config slice (const String& key) const; + const Config slice (const String& key) const noexcept; + + /** + * Query for sections in this `Config` instance. The `query` can contain + * valid regular expression useful for matching sections, keys, and values. + * + * @param query The query to filter sections, keys, and values + * @return A `const Config` with sections, keys, and values matched by `query` + * + * @example + * const auto config = Config::getUserConfig(); + * const auto icons = config.query("*icon="); + */ + const Config query (const String& query) const noexcept; + + /** + * Get a vector all configuration keys. + * + * @return A `const Vector` of all configuration keys. + */ + const Vector<String> keys () const noexcept; /** * Get a configuration value by name or `.` path using `[]` notation. + * * @param key The key to look up a value for. * @return The value at `key` */ @@ -131,6 +169,7 @@ namespace SSC { /** * Get a configuration value reference by name or `.` path * using `[]` notation. + * * @param key The key to look up a value for. * @return A reference to the value at `key` */ @@ -138,15 +177,37 @@ namespace SSC { /** * Get the beginning of iterator to the configuration tuples. + * * @return `Iterator` */ - Iterator begin (); + const Iterator begin () const noexcept; /** * Get the end of iterator to the configuration tuples. + * * @return `Iterator` */ - Iterator end (); + const Iterator end () const noexcept; + + /** + * Get the number of entries in the configuration. + * + * @return The number of entires in the configuration. + */ + const std::size_t size () const noexcept; + + /** + * Clears all entries in the configuration. + * @return `true` upon success, otherwise `false`. + */ + const bool clear () noexcept; + + /** + * Get a vector of configuration children as "slices". + * + * @return Child configuration for this configuration. + */ + const Vector<Config> children () const noexcept; }; } #endif From b000bcec833b651e12acf3c236fe41d5e3042a41 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 18 Oct 2023 10:57:25 -0400 Subject: [PATCH 227/256] refactor(core/string): improve join --- src/core/string.cc | 33 ++++++++++++++++++++++++++++++--- src/core/string.hh | 6 ++++-- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/core/string.cc b/src/core/string.cc index 4062e0a190..b22877b5a2 100644 --- a/src/core/string.cc +++ b/src/core/string.cc @@ -1,4 +1,6 @@ #include "string.hh" +#include "debug.hh" + #include <regex> namespace SSC { @@ -22,7 +24,28 @@ namespace SSC { return output; } - const Vector<String> split (const String& source, const char& character) { + const Vector<String> split (const String& source, const String& needle) { + Vector<String> result; + String current = source; + size_t position = 0; + + while (current.size() > 0 && position < source.size()) { + position = current.find(needle); + if (position == std::string::npos) { + result.push_back(current); + break; + } + + const auto string = current.substr(0, position); + current = current.substr(std::min(current.size(), position + needle.size())); + result.push_back(string); + position += needle.size(); + } + + return result; + } + + const Vector<String> split (const String& source, const char character) { String buffer; Vector<String> result; @@ -42,7 +65,7 @@ namespace SSC { return result; } - const Vector<String> splitc (const String& source, const char& character) { + const Vector<String> splitc (const String& source, const char character) { String buffer; Vector<String> result; @@ -93,13 +116,17 @@ namespace SSC { for (const auto& item : vector) { joined << item; if (--missing > 0) { - joined << separator << " "; + joined << separator; } } return trim(joined.str()); } + const String join (const Vector<String>& vector, const char separator) { + return join(vector, String(1, separator)); + } + Vector<String> parseStringList (const String& string, const Vector<char>& separators) { auto list = Vector<String>(); for (const auto& separator : separators) { diff --git a/src/core/string.hh b/src/core/string.hh index 762196a55d..40f26fb960 100644 --- a/src/core/string.hh +++ b/src/core/string.hh @@ -25,9 +25,11 @@ namespace SSC { String convertWStringToString (const String& source); // vector parsers - const Vector<String> split (const String& source, const char& character); - const Vector<String> splitc (const String& source, const char& character); + const Vector<String> splitc (const String& source, const char character); + const Vector<String> split (const String& source, const char character); + const Vector<String> split (const String& source, const String& needle); const String join (const Vector<String>& vector, const String& separator); + const String join (const Vector<String>& vector, const char separator); Vector<String> parseStringList (const String& string, const Vector<char>& separators); Vector<String> parseStringList (const String& string, const char separator); Vector<String> parseStringList (const String& string); From 80a1552ca5107f2a74f59ac490e56b1ef4f39912 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 18 Oct 2023 10:57:44 -0400 Subject: [PATCH 228/256] refactor(src/core/json.hh): use types in declaration --- src/core/json.hh | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/core/json.hh b/src/core/json.hh index 671a2cf535..744362ee18 100644 --- a/src/core/json.hh +++ b/src/core/json.hh @@ -55,7 +55,7 @@ namespace SSC::JSON { Type type = t; D data; - auto typeof () const { + const SSC::String typeof () const { switch (this->type) { case Type::Empty: return SSC::String("empty"); case Type::Raw: return SSC::String("raw"); @@ -69,14 +69,14 @@ namespace SSC::JSON { } } - auto isRaw() const { return this->type == Type::Raw; } - auto isArray () const { return this->type == Type::Array; } - auto isBoolean () const { return this->type == Type::Boolean; } - auto isNumber () const { return this->type == Type::Number; } - auto isNull () const { return this->type == Type::Null; } - auto isObject () const { return this->type == Type::Object; } - auto isString () const { return this->type == Type::String; } - auto isEmpty () const { return this->type == Type::Empty; } + bool isRaw() const { return this->type == Type::Raw; } + bool isArray () const { return this->type == Type::Array; } + bool isBoolean () const { return this->type == Type::Boolean; } + bool isNumber () const { return this->type == Type::Number; } + bool isNull () const { return this->type == Type::Null; } + bool isObject () const { return this->type == Type::Object; } + bool isString () const { return this->type == Type::String; } + bool isEmpty () const { return this->type == Type::Empty; } }; class Null : public Value<std::nullptr_t, Type::Null> { From 9ecd80a01488c1e2f408bedd0ee8c541f1d9f0a7 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 18 Oct 2023 10:57:58 -0400 Subject: [PATCH 229/256] refactor(src/core/types.hh): 'Exception' type --- src/core/types.hh | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/types.hh b/src/core/types.hh index f9c5912434..7b4f041fac 100644 --- a/src/core/types.hh +++ b/src/core/types.hh @@ -32,6 +32,7 @@ namespace SSC { using Mutex = std::recursive_mutex; using Lock = std::lock_guard<Mutex>; using Thread = std::thread; + using Exception = std::exception; template <typename T> using Atomic = std::atomic<T>; template <typename T, int k> using Array = std::array<T, k>; From f94a362d84674148272c46f21cb00968de27cad9 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 18 Oct 2023 10:58:47 -0400 Subject: [PATCH 230/256] refactor(test/socket.ini): include env var for testing --- test/socket.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/socket.ini b/test/socket.ini index 4fd2efc53a..2540fd8b5e 100644 --- a/test/socket.ini +++ b/test/socket.ini @@ -16,6 +16,7 @@ env[] = DEBUG env[] = SSC_ANDROID_CI env[] = SOCKET_DEBUG_IPC env[] = SOCKET_MODULE_PATH_PREFIX +env[] = TEST_INJECTED_VARIABLE [build.extensions] runtime-core-tests = src/runtime-core @@ -25,6 +26,7 @@ sqlite3 = src/extensions/sqlite3 ; Injected environment variables [env] SOCKET_MODULE_PATH_PREFIX = "node_modules" +TEST_INJECTED_VARIABLE = "TEST_INJECTED_VARIABLE" ; Package Metadata [meta] From db08f39f8faa071906afd7c6ff0de64c63d6dbee Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 18 Oct 2023 10:59:02 -0400 Subject: [PATCH 231/256] test(language): reduce test spam --- test/src/language.js | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/test/src/language.js b/test/src/language.js index 8ee5c59322..00e41986e9 100644 --- a/test/src/language.js +++ b/test/src/language.js @@ -2,31 +2,48 @@ import language from 'socket:language' import test from 'socket:test' test('language.lookup(query[, options])', (t) => { + const pass = { + names: true, + codes: true + } + for (const name of language.names) { const results = language.lookup(name, { strict: true }) - t.ok(results.length === 1, 'exactly 1 result in strict lookup by name: ' + name) - t.ok(results[0]?.name === name, 'result.name === name') - t.ok(results[0]?.code, 'result.code') - t.ok(results[0]?.tags.length, 'results[0].tags') + pass.names = pass.name && ( + results.length && + results[0]?.name && + results[0]?.code && + results[0]?.tags.length + ) } for (const code of language.codes) { const results = language.lookup(code, { strict: true }) - t.ok(results.length === 1, 'exactly 1 result in strict lookup by code: ' + code) - t.ok(results[0].code === code, 'result.code === code') - t.ok(results[0].name, 'result.name') - t.ok(results[0].tags.length, 'results[0].tags') + pass.codes = pass.codes && ( + results.length === 1 && + results[0].code === code && + results[0].name && + results[0].tags.length + ) } + + t.ok(pass.names, 'lookup by name') + t.ok(pass.codes, 'lookup by code') }) test('language.describe(tag[, options])', (t) => { + let pass = true for (const tag of language.tags) { const results = language.describe(tag, { strict: true }) if (results.length) { - t.ok(results.length, 'results.length > 0') - t.ok(results[0]?.tag === tag, 'result.tag === ' + tag) - t.ok(results[0]?.code, 'result.code') - t.ok(results[0]?.description, 'result.description') + pass = pass && ( + results.length && + results[0]?.tag === tag && + results[0]?.code && + results[0]?.description + ) } } + + t.ok(pass, 'language descriptions') }) From b007f85759a1d370f2f3bfb1be035cc4c5db7395 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 18 Oct 2023 10:59:17 -0400 Subject: [PATCH 232/256] test(runtime-core): more runtime core tests --- test/src/runtime-core/codec.cc | 2 +- test/src/runtime-core/config.cc | 163 +++++++++++++++++++++++++++++- test/src/runtime-core/env.cc | 13 ++- test/src/runtime-core/harness.cc | 132 ++++++++++++++++++------ test/src/runtime-core/ini.cc | 4 +- test/src/runtime-core/json.cc | 33 +++++- test/src/runtime-core/main.cc | 1 + test/src/runtime-core/main.js | 2 +- test/src/runtime-core/platform.cc | 2 +- test/src/runtime-core/preload.cc | 3 +- test/src/runtime-core/socket.ini | 8 +- test/src/runtime-core/string.cc | 54 ++++++++++ test/src/runtime-core/tests.hh | 60 +++++++---- test/src/runtime-core/version.cc | 7 +- 14 files changed, 417 insertions(+), 67 deletions(-) create mode 100644 test/src/runtime-core/string.cc diff --git a/test/src/runtime-core/codec.cc b/test/src/runtime-core/codec.cc index 41f4d6c239..aa16f1cd5a 100644 --- a/test/src/runtime-core/codec.cc +++ b/test/src/runtime-core/codec.cc @@ -2,7 +2,7 @@ #include "src/core/codec.hh" namespace SSC::Tests { - void codec (const Harness& t) { + void codec (Harness& t) { t.test("SSC::encodeURIComponent", [](auto t) { const auto encoded = SSC::encodeURIComponent( "a % encoded string with foo@bar.com, $100, & #tag" diff --git a/test/src/runtime-core/config.cc b/test/src/runtime-core/config.cc index 93bb921439..aa904e59bd 100644 --- a/test/src/runtime-core/config.cc +++ b/test/src/runtime-core/config.cc @@ -2,8 +2,167 @@ #include "src/core/config.hh" namespace SSC::Tests { - void config (const Harness& t) { - t.test("SSC::Config", [](auto t) { + void config (Harness& t) { + t.test("SSC::Config::get()", [](auto t) { + const auto config = Config(R"INI( + [a] + key = "value" + + [b] + key = "value" + )INI"); + + const auto a = config.get("a.key"); + const auto b = config.get("b.key"); + + t.equals(a, "value", "a.key == value"); + t.equals(b, "value", "b.key == value"); + }); + + t.test("SSC::Config::set()", [](auto t) { + Config config; + config.set("a.key", "value"); + config.set("b.key", "value"); + t.equals(config.get("a.key"), "value", "a.key == value"); + t.equals(config.get("b.key"), "value", "b.key == value"); + }); + + t.test("SSC::Config::contains()", [](auto t) { + Config config; + config.set("a.key", "value"); + config.set("b.key", "value"); + t.assert(config.contains("a.key"), "contains a.key"); + t.assert(config.contains("b.key"), "contains b.key"); + }); + + t.test("SSC::Config::query()", [](auto t) { + const auto config = Config(R"INI( + [simple] + key = "value" + + [first.class-a] + key = "class a" + + [second.class-a] + key = "class a" + + [third.class-a] + key = "class a" + )INI"); + + auto simple = config.query("[simple]"); + auto first = config.query("[first]"); + auto second = config.query("[second]"); + auto third = config.query("[third]"); + + auto classA = config.query("[.class-a]"); + + t.equals(simple.get("simple.key"), "value", "simple.key == value"); + t.equals(first.get("first.class-a.key"), "class a", "first.class-a.key == class a"); + t.equals(second.get("second.class-a.key"), "class a", "second.class-a.key == class a"); + t.equals(third.get("third.class-a.key"), "class a", "third.class-a.key == class a"); + + for (const auto& tuple : classA) { + t.equals(tuple.second, "class a", tuple.first + "contains [.class-a]"); + } + }); + + t.test("SSC::Config::erase()", [](auto t) { + Config config; + config.set("a.key", "value"); + config.set("b.key", "value"); + + config.erase("a.key"); + t.assert(!config.contains("a.key"), "does not contain a.key"); + + config.erase("b"); + t.assert(!config.contains("b.key"), "does not contain b.key"); + }); + + t.test("SSC::Config::clear()", [](auto t) { + Config config; + config.set("a.key", "value"); + config.set("b.key", "value"); + + config.clear(); + t.assert(!config.contains("a.key"), "does not contain a.key"); + t.assert(!config.contains("b.key"), "does not contain b.key"); + }); + + t.test("SSC::Config::size()", [](auto t) { + Config config; + config.set("a", "value"); + t.equals(config.size(), 1, "config.size() == 1"); + config.set("b", "value"); + t.equals(config.size(), 2, "config.size() == 2"); + config.set("c", "value"); + t.equals(config.size(), 3, "config.size() == 3"); + config.set("c", "value"); + t.equals(config.size(), 3, "config.size() == 3"); + config.erase("c"); + t.equals(config.size(), 2, "config.size() == 2"); + }); + + t.test("SSC::Config::slice()", [](auto t) { + const auto config = Config(R"INI( + [meta] + title = "my application" + version = "1.2.3" + + [build] + script = "build.sh" + + [build.extensions.my-extension] + source = "extension/" + + [build.extensions.my-other-extension] + source = "other-extension/" + )INI"); + + const auto meta = config.slice("meta"); + t.equals(meta.get("title"), "my application", "meta.title = 'my application'"); + t.equals(meta.get("version"), "1.2.3", "meta.version = '1.2.3'"); + + const auto build = config.slice("build"); + t.equals(build.get("script"), "build.sh", "build.script == 'build.sh'"); + + const auto extensions = build.slice("extensions"); + t.equals(extensions.get("my-extension.source"), "extension/", "build.extensions.my-extension.source = 'extension/'"); + t.equals(extensions.get("my-other-extension.source"), "other-extension/", "build.extensions.my-other-extension.source = 'other-extension/'"); + }); + + t.test("SSC::Config::children()", [](auto t) { + const auto config = Config(R"INI( + [0] + leaf = 0 + [0.1] + leaf = 01 + [0.1.0] + leaf = 010 + [0.1.1] + leaf = 011 + [0.1.2] + leaf = 012 + [0.2] + leaf = 02 + [0.3] + leaf = 03 + + [1] + leaf = 1 + + [2] + leaf = 2 + )INI"); + + const auto children = config.children(); + t.equals(children[0].prefix, "0", "children[0].prefix == 0"); + t.equals(children[1].prefix, "1", "children[1].prefix == 1"); + t.equals(children[2].prefix, "2", "children[2].prefix == 2"); + + t.equals(children[0].children()[0].prefix, "1", "children[0].children[0].prefix == 1"); + t.equals(children[0].children()[1].prefix, "2", "children[0].children[1].prefix == 2"); + t.equals(children[0].children()[2].prefix, "3", "children[0].children[2].prefix == 3"); }); } } diff --git a/test/src/runtime-core/env.cc b/test/src/runtime-core/env.cc index 9acc5cf85b..0feba28d95 100644 --- a/test/src/runtime-core/env.cc +++ b/test/src/runtime-core/env.cc @@ -1,6 +1,17 @@ #include "tests.hh" namespace SSC::Tests { - void env (const Harness& t) { + void env (Harness& t) { + t.test("SSC::Env::get()", [](auto t) { + const auto TEST_INJECTED_VARIABLE = SSC::Env::get("TEST_INJECTED_VARIABLE"); + const auto HOME = SSC::Env::get("HOME"); + t.equals( + TEST_INJECTED_VARIABLE, + "TEST_INJECTED_VARIABLE", + "TEST_INJECTED_VARIABLE env var is set" + ); + + t.assert(HOME, "HOME env var is set"); + }); } } diff --git a/test/src/runtime-core/harness.cc b/test/src/runtime-core/harness.cc index f358798a9f..e35c99b9b7 100644 --- a/test/src/runtime-core/harness.cc +++ b/test/src/runtime-core/harness.cc @@ -3,46 +3,50 @@ #include "./ok.hh" namespace SSC::Tests { - static Harness::Mutex mutex; - static Atomic<int> pending = 0; - - Harness::Harness () { - mutex.unlock(); - } - Harness::Harness (const Options& options) : options(options) { - mutex.unlock(); + this->pending = this->options.pending; + this->isAsync = this->options.isAsync; } - bool Harness::run (TestRunner runner) const { - return run(false, "", runner); + bool Harness::run (TestRunner runner) { + return this->run(false, "", runner); } - bool Harness::run (bool isAsync, TestRunner runner) const { - return run(isAsync, "", runner); + bool Harness::run (bool isAsync, TestRunner runner) { + return this->run(isAsync, "", runner); } - bool Harness::run (bool isAsync, const String& label, TestRunner runner) const { + bool Harness::run (bool isAsync, const String& label, TestRunner runner) { if (label.size() > 0) { - mutex.lock(); + this->mutex.lock(); this->label(label); if (this->options.resetContextAfterEachRun) { ok_reset(); } } - pending++; - runner(*this); - pending--; + this->pending++; + Harness harness(Options { + .resetContextAfterEachRun = this->options.resetContextAfterEachRun, + .isAsync = isAsync, + .pending = this->pending.load() + }); + + runner(harness); + this->pending--; if (label.size() > 0 ) { - if (!isAsync) { - mutex.unlock(); + if (!this->isAsync) { + this->mutex.unlock(); } } - if (pending == 0) { - if (ok_count() > 0 || ok_failed() > 0 || ok_expected() > 0) { + if (harness.isAsync) { + harness.wait(); + } + + if (this->pending == 0) { + if (ok_count() > 0 || ok_failed() > 0) { auto success = ok_done(); ok_reset(); return success; @@ -52,15 +56,31 @@ namespace SSC::Tests { return false; } - void Harness::end () const { - mutex.unlock(); + void Harness::end () { + this->mutex.unlock(); + } + + void Harness::wait () { + this->mutex.lock(); + this->mutex.unlock(); + } + + void Harness::plan (unsigned int planned) { + if (planned > 0) { + this->isAsync = true; + this->mutex.lock(); + } else if (planned == 0 && this->isAsync) { + this->isAsync = false; + } + + this->planned = planned; } - bool Harness::test (const String& label, bool isAsync, TestRunner scope) const { + bool Harness::test (const String& label, bool isAsync, TestRunner scope) { return this->run(isAsync, label, scope); } - bool Harness::test (const String& label, TestRunner scope) const { + bool Harness::test (const String& label, TestRunner scope) { return this->run(false, label, scope); } @@ -77,6 +97,36 @@ namespace SSC::Tests { sapi_log(0, message.c_str()); } + void Harness::log (const Map& message) const { + if (message.size() == 0) { + return this->log("Map {}"); + } + + auto size = message.size(); + int i = 0; + this->log("Map {"); + for (const auto& tuple : message) { + const auto postfix = ++i < size ? "," : ""; + this->log(" \"" + tuple.first + "\" = \"" + tuple.second + "\""+ postfix); + } + this->log("}"); + } + + void Harness::log (const Vector<String>& message) const { + if (message.size() == 0) { + return this->log("Vector<String> {}"); + } + + auto size = message.size(); + int i = 0; + this->log("Vector<String> {"); + for (const auto& item: message) { + const auto postfix = ++i < size ? "," : ""; + this->log(" " + item + postfix); + } + this->log("}"); + } + bool Harness::assert (bool assertion, const String& message) const { if (assertion) { ok("%s", message.c_str()); @@ -88,23 +138,23 @@ namespace SSC::Tests { } bool Harness::assert (int64_t value, const String& message) const { - return assert(value != 0, message); + return this->assert(value != 0, message); } bool Harness::assert (double value, const String& message) const { - return assert(value != 0.0, message); + return this->assert(value != 0.0, message); } bool Harness::assert (void* value, const String& message) const { - return assert(value != 0, message); + return this->assert(value != 0, message); } bool Harness::assert (const String& value, const String& message) const { - return assert(value.size() != 0, message); + return this->assert(value.size() != 0, message); } bool Harness::equals (const char* left, const char* right, const String& message) const { - return equals(String(left), String(right), message); + return this->equals(String(left), String(right), message); } bool Harness::equals (const String& left, const String& right, const String& message) const { @@ -147,6 +197,16 @@ namespace SSC::Tests { } } + bool Harness::equals (const size_t left, const size_t right, const String& message) const { + if (left == right) { + ok("%zu equals %zu: %s", left, right, message.c_str()); + return true; + } else { + notok("%zu does not equal %zu: %s", left, right, message.c_str()); + return false; + } + } + bool Harness::notEquals (const String& left, const String& right, const String& message) const { if (left == right) { notok("'%s' equals '%s': %s", left.c_str(), right.c_str(), message.c_str()); @@ -158,7 +218,7 @@ namespace SSC::Tests { } bool Harness::notEquals (const char* left, const char* right, const String& message) const { - return notEquals(String(left), String(right), message); + return this->notEquals(String(left), String(right), message); } bool Harness::notEquals (const int64_t left, const int64_t right, const String& message) const { @@ -181,6 +241,16 @@ namespace SSC::Tests { } } + bool Harness::notEquals (const size_t left, const size_t right, const String& message) const { + if (left == right) { + notok("%zu equals %zu: %s", left, right, message.c_str()); + return false; + } else { + ok("%zu does not equal %zu: %s", left, right, message.c_str()); + return true; + } + } + bool Harness::throws (std::function<void()> fn, const String& message) const { try { fn(); diff --git a/test/src/runtime-core/ini.cc b/test/src/runtime-core/ini.cc index 75f49b8708..fb3c344937 100644 --- a/test/src/runtime-core/ini.cc +++ b/test/src/runtime-core/ini.cc @@ -1,7 +1,7 @@ #include "tests.hh" namespace SSC::Tests { - void ini (const Harness& t) { + void ini (Harness& t) { t.test("SSC::INI::parse", [] (auto t) { auto simple = SSC::INI::parse(R"INI( key = "value" @@ -57,7 +57,7 @@ namespace SSC::Tests { [strings] array[] = "hello" - array[] = "world" + array[] = world )INI"); t.equals(arrays["numbers_array"], "1 2 3", "arrays[numbers_array] == 1 2 3"); diff --git a/test/src/runtime-core/json.cc b/test/src/runtime-core/json.cc index 1b622fc164..70d3b16d73 100644 --- a/test/src/runtime-core/json.cc +++ b/test/src/runtime-core/json.cc @@ -1,6 +1,37 @@ #include "tests.hh" namespace SSC::Tests { - void json (const Harness& t) { + void json (Harness& t) { + t.test("SSC::JSON::Any", [](auto t) { + t.comment("TODO"); + }); + + t.test("SSC::JSON::Raw", [](auto t) { + t.comment("TODO"); + }); + + t.test("SSC::JSON::Null", [](auto t) { + t.comment("TODO"); + }); + + t.test("SSC::JSON::Object", [](auto t) { + t.comment("TODO"); + }); + + t.test("SSC::JSON::Array", [](auto t) { + t.comment("TODO"); + }); + + t.test("SSC::JSON::Boolean", [](auto t) { + t.comment("TODO"); + }); + + t.test("SSC::JSON::Number", [](auto t) { + t.comment("TODO"); + }); + + t.test("SSC::JSON::String", [](auto t) { + t.comment("TODO"); + }); } } diff --git a/test/src/runtime-core/main.cc b/test/src/runtime-core/main.cc index 36979b74c8..6ca7a38561 100644 --- a/test/src/runtime-core/main.cc +++ b/test/src/runtime-core/main.cc @@ -11,6 +11,7 @@ static bool initialize (sapi_context_t* context, const void *data) { t.run(SSC::Tests::json); t.run(SSC::Tests::platform); t.run(SSC::Tests::preload); + t.run(SSC::Tests::string); t.run(SSC::Tests::version); }); } diff --git a/test/src/runtime-core/main.js b/test/src/runtime-core/main.js index 6c303808d3..eb6dd39a2d 100644 --- a/test/src/runtime-core/main.js +++ b/test/src/runtime-core/main.js @@ -1,7 +1,7 @@ import extension from 'socket:extension' import process from 'socket:process' -const EXIT_TIMEOUT = 250 +const EXIT_TIMEOUT = 500 try { await extension.load('runtime-core-tests') diff --git a/test/src/runtime-core/platform.cc b/test/src/runtime-core/platform.cc index 7d0fb2b963..300f8e2984 100644 --- a/test/src/runtime-core/platform.cc +++ b/test/src/runtime-core/platform.cc @@ -2,7 +2,7 @@ #include "src/core/platform.hh" namespace SSC::Tests { - void platform (const Harness& t) { + void platform (Harness& t) { t.test("SSC::platform.arch", [](auto t) { t.assert(SSC::platform.arch, "SSC::platform.arch is not empty"); #if defined(__x86_64__) || defined(_M_X64) diff --git a/test/src/runtime-core/preload.cc b/test/src/runtime-core/preload.cc index 3255aa4707..5646a031e5 100644 --- a/test/src/runtime-core/preload.cc +++ b/test/src/runtime-core/preload.cc @@ -1,6 +1,7 @@ #include "tests.hh" namespace SSC::Tests { - void preload (const Harness& t) { + void preload (Harness& t) { + t.assert(createPreload(WindowOptions {}), "createPreload() returns non-empty string");; } } diff --git a/test/src/runtime-core/socket.ini b/test/src/runtime-core/socket.ini index aad94bc4f7..890431db8f 100644 --- a/test/src/runtime-core/socket.ini +++ b/test/src/runtime-core/socket.ini @@ -17,11 +17,13 @@ sources[] = ./ini.cc sources[] = ./json.cc sources[] = ./platform.cc sources[] = ./preload.cc +sources[] = ./string.cc sources[] = ./version.cc [extension.compiler] -flags[] = -fsanitize=undefined-trap +flags[] = -I../../.. + +[extension.mac.compiler] flags[] = -fsanitize-undefined-trap-on-error +flags[] = -fsanitize=undefined-trap flags[] = -ftrap-function=abort -flags[] = -DLIBOK_PRINTF_NEEDS_NEWLINE=0 -flags[] = -I../../.. diff --git a/test/src/runtime-core/string.cc b/test/src/runtime-core/string.cc new file mode 100644 index 0000000000..9b5f054e91 --- /dev/null +++ b/test/src/runtime-core/string.cc @@ -0,0 +1,54 @@ +#include "tests.hh" + +namespace SSC::Tests { + void string (Harness& t) { + t.test("SSC::replace()", [](auto t) { + t.comment("TODO"); + }); + + t.test("SSC::tmpl()", [](auto t) { + t.comment("TODO"); + }); + + t.test("SSC::trim()", [](auto t) { + t.comment("TODO"); + }); + + t.test("SSC::convertStringToWString()", [](auto t) { + t.comment("TODO"); + }); + + t.test("SSC::convertWStringToString()", [](auto t) { + t.comment("TODO"); + }); + + t.test("SSC::split(const String&, const String&)", [](auto t) { + const auto items = split("a && b && c && d && e", " && "); + + t.equals(items[0], "a", "items[0] == a"); + t.equals(items[1], "b", "items[1] == b"); + t.equals(items[2], "c", "items[2] == c"); + t.equals(items[3], "d", "items[3] == d"); + t.equals(items[4], "e", "items[4] == e"); + }); + + t.test("SSC::split(const String&, char)", [](auto t) { + const auto items = split("a|b|c|d|e", '|'); + + t.equals(items[0], "a", "items[0] == a"); + t.equals(items[1], "b", "items[1] == b"); + t.equals(items[2], "c", "items[2] == c"); + t.equals(items[3], "d", "items[3] == d"); + t.equals(items[4], "e", "items[4] == e"); + }); + + t.test("SSC::join()", [](auto t) { + const auto joined = join(split("a|b|c|d|e", '|'), '|'); + t.equals(joined, "a|b|c|d|e", "joins vector"); + }); + + t.test("SSC::parseStringList()", [](auto t) { + t.comment("TODO"); + }); + } +} diff --git a/test/src/runtime-core/tests.hh b/test/src/runtime-core/tests.hh index d141644cd6..4db1483089 100644 --- a/test/src/runtime-core/tests.hh +++ b/test/src/runtime-core/tests.hh @@ -10,17 +10,24 @@ namespace SSC::Tests { class Harness; - typedef void (TestRunner)(const Harness& harness); + typedef void (TestRunner)(Harness& harness); class Harness { public: + using Mutex = std::mutex; struct Options { bool resetContextAfterEachRun = false; + bool isAsync = false; + int pending = 0; }; - using Mutex = std::mutex; const Options options; - Harness (); + Mutex mutex; + Atomic<bool> isAsync = false; + Atomic<int> pending = 0; + Atomic<unsigned int> planned = 0; + + Harness () = default; Harness (const Options& options); bool assert (bool assertion, const String& message = "") const; @@ -31,37 +38,46 @@ namespace SSC::Tests { bool equals (const String& left, const String& right, const String& message) const; bool equals (const char* left, const char* right, const String& message) const; - bool equals (const bool left, const bool right, const String& message) const; - bool equals (const int64_t left, const int64_t right, const String& message) const; - bool equals (const double left, const double right, const String& message) const; + bool equals (bool left, bool right, const String& message) const; + bool equals (int64_t left, int64_t right, const String& message) const; + bool equals (double left, double right, const String& message) const; + bool equals (size_t left, size_t right, const String& message) const; bool notEquals (const String& left, const String& right, const String& message) const; bool notEquals (const char* left, const char* right, const String& message) const; - bool notEquals (const int64_t left, const int64_t right, const String& message) const; - bool notEquals (const double left, const double right, const String& message) const; + bool notEquals (int64_t left, int64_t right, const String& message) const; + bool notEquals (double left, double right, const String& message) const; + bool notEquals (size_t left, size_t right, const String& message) const; bool throws (std::function<void()> fn, const String& message) const; void comment (const String& comment) const; void label (const String& label) const; - bool test (const String& label, TestRunner scope) const; - bool test (const String& label, bool isAsync, TestRunner scope) const; void log (const String& message) const; - bool run (bool isAsync, const String& label, TestRunner runner) const; - bool run (bool isAsync, TestRunner runner) const; - bool run (TestRunner) const; - void end () const; + void log (const Map& message) const; + void log (const Vector<String>& message) const; + + void plan (unsigned int count); + + bool test (const String& label, TestRunner scope); + bool test (const String& label, bool isAsync, TestRunner scope); + bool run (bool isAsync, const String& label, TestRunner runner); + bool run (bool isAsync, TestRunner runner); + bool run (TestRunner); + void end (); + void wait (); }; // tests - void codec (const Harness&); - void config (const Harness&); - void env (const Harness&); - void ini (const Harness&); - void json (const Harness&); - void platform (const Harness&); - void preload (const Harness&); - void version (const Harness&); + void codec (Harness&); + void config (Harness&); + void env (Harness&); + void ini (Harness&); + void json (Harness&); + void platform (Harness&); + void preload (Harness&); + void string (Harness&); + void version (Harness&); } #endif diff --git a/test/src/runtime-core/version.cc b/test/src/runtime-core/version.cc index 07d6cedfbc..d2f938edcb 100644 --- a/test/src/runtime-core/version.cc +++ b/test/src/runtime-core/version.cc @@ -1,6 +1,11 @@ #include "tests.hh" namespace SSC::Tests { - void version (const Harness& t) { + void version (Harness& t) { + t.test("SSC::{VERSION_FULL_STRING,VERSION_HASH_STRING,VERSION_STRING}", [](auto t) { + t.assert(VERSION_FULL_STRING, "VERSION_FULL_STRING is defined"); + t.assert(VERSION_HASH_STRING, "VERSION_HASH_STRING is defined"); + t.assert(VERSION_STRING, "VERSION_STRING is defined"); + }); } } From 5eccfd26bdb49b8e9b783440caadd89120ee1ecb Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 18 Oct 2023 11:24:34 -0400 Subject: [PATCH 233/256] test(runtime-core): fix default 'Harness' constructor --- test/src/runtime-core/harness.cc | 2 ++ test/src/runtime-core/tests.hh | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/src/runtime-core/harness.cc b/test/src/runtime-core/harness.cc index e35c99b9b7..8016c60889 100644 --- a/test/src/runtime-core/harness.cc +++ b/test/src/runtime-core/harness.cc @@ -3,6 +3,8 @@ #include "./ok.hh" namespace SSC::Tests { + Harness::Harness () : options() {} + Harness::Harness (const Options& options) : options(options) { this->pending = this->options.pending; this->isAsync = this->options.isAsync; diff --git a/test/src/runtime-core/tests.hh b/test/src/runtime-core/tests.hh index 4db1483089..e4d092c841 100644 --- a/test/src/runtime-core/tests.hh +++ b/test/src/runtime-core/tests.hh @@ -27,7 +27,7 @@ namespace SSC::Tests { Atomic<int> pending = 0; Atomic<unsigned int> planned = 0; - Harness () = default; + Harness (); Harness (const Options& options); bool assert (bool assertion, const String& message = "") const; From 98c02de033dae9cd6c878b69ffa77a2274c534fa Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 18 Oct 2023 11:35:31 -0400 Subject: [PATCH 234/256] fix(cli): fix missing linux flags for extension --- src/cli/cli.cc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 2e55054ecb..b9f7b66dd2 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -5187,6 +5187,11 @@ int main (const int argc, const char* argv[]) { compilerFlags += " -framework OSLog"; } + if (platform.linux) { + compilerFlags += " -std=c++2a"; + compilerFlags += " " + exec("pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.1").output; + } + if (platform.win && debugBuild) { compilerDebugFlags += "-D_DEBUG"; } From 94e65e4d5a718789deea67ccaea9fb6ca29d8164 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 18 Oct 2023 11:37:09 -0400 Subject: [PATCH 235/256] fix(core/string): undef 'min' macro --- src/core/string.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/string.cc b/src/core/string.cc index b22877b5a2..4cd553e340 100644 --- a/src/core/string.cc +++ b/src/core/string.cc @@ -3,6 +3,10 @@ #include <regex> +#if defined(min) +#undef min +#endif + namespace SSC { String replace (const String& source, const std::regex& regex, const String& value) { return std::regex_replace(source, regex, value); From acc26b9f7a10960510a18c58cba2639c27b8de27 Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 18 Oct 2023 11:58:45 -0400 Subject: [PATCH 236/256] fix(cli): fix linux flags for extension --- src/cli/cli.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index b9f7b66dd2..47c64c6c8d 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -5189,7 +5189,7 @@ int main (const int argc, const char* argv[]) { if (platform.linux) { compilerFlags += " -std=c++2a"; - compilerFlags += " " + exec("pkg-config --cflags --libs gtk+-3.0 webkit2gtk-4.1").output; + compilerFlags += " " + exec("pkg-config --cflags gtk+-3.0 webkit2gtk-4.1").output; } if (platform.win && debugBuild) { From 8297224b7f57d43824ebdf238048974879c70f2b Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 18 Oct 2023 12:33:22 -0400 Subject: [PATCH 237/256] refactor(cli,core/platform): prevent 'WebView2' inclusion when building extension --- src/cli/cli.cc | 16 +++++++++++++++- src/core/platform.hh | 3 +++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 47c64c6c8d..9dac0e7b2d 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -3448,6 +3448,8 @@ int main (const int argc, const char* argv[]) { std::unordered_set<String> cflags; std::unordered_set<String> cppflags; + compilerFlags += " -DSOCKET_RUNTIME_EXTENSION"; + if (path.size() > 0) { fs::current_path(targetPath); fs::current_path(path); @@ -4030,6 +4032,8 @@ int main (const int argc, const char* argv[]) { settings["build_extensions_" + extension + "_ios_compiler_debug_flags"] + " " ); + compilerFlags += " -DSOCKET_RUNTIME_EXTENSION"; + // --platform=ios should always build for arm64 even on Darwin x86_64 if (!flagBuildForSimulator) { compilerFlags += " -arch arm64 "; @@ -5176,6 +5180,8 @@ int main (const int argc, const char* argv[]) { settings["build_extensions_" + extension + "_" + os + "_compiler_debug_flags"] + " " ); + compilerFlags += " -DSOCKET_RUNTIME_EXTENSION"; + if (platform.mac) { compilerFlags += " -framework UniformTypeIdentifiers"; compilerFlags += " -framework CoreBluetooth"; @@ -5189,7 +5195,15 @@ int main (const int argc, const char* argv[]) { if (platform.linux) { compilerFlags += " -std=c++2a"; - compilerFlags += " " + exec("pkg-config --cflags gtk+-3.0 webkit2gtk-4.1").output; + compilerFlags += " " + trim(exec("pkg-config --cflags gtk+-3.0 webkit2gtk-4.1").output); + } + + if (platform.win) { + auto prefix = prefixFile(); + compilerFlags += " -I\"" + Path(paths.platformSpecificOutputPath / "include").string() + "\""; + compilerFlags += " -I\"" + prefix + "include\""; + compilerFlags += " -I\"" + prefix + "src\""; + compilerFlags += " -L\"" + prefix + "lib\""; } if (platform.win && debugBuild) { diff --git a/src/core/platform.hh b/src/core/platform.hh index 6de1c906ac..bfec54c668 100644 --- a/src/core/platform.hh +++ b/src/core/platform.hh @@ -70,8 +70,11 @@ #include <shlobj_core.h> #include <shobjidl.h> +#if !defined(SOCKET_RUNTIME_EXTENSION) #include <WebView2.h> #include <WebView2EnvironmentOptions.h> +#endif + #include <shellapi.h> #pragma comment(lib, "advapi32.lib") From 3a0ae40c358c5289945f56062899f7b2ee971bba Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Wed, 18 Oct 2023 13:54:15 -0400 Subject: [PATCH 238/256] fix(cli): fix missing '--env' flag and linux ext cflags --- src/cli/cli.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 9dac0e7b2d..734e41f313 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -2561,6 +2561,7 @@ int main (const int argc, const char* argv[]) { { { "--headless", "-H" }, true, false }, { { "--debug", "-D" }, true, false }, { { "--verbose", "-V" }, true, false }, + { { "--env", "-E" }, true, true } }; Options buildOptions = { @@ -2574,7 +2575,8 @@ int main (const int argc, const char* argv[]) { { { "--package", "-p" }, true, false }, { { "--package-format", "-f" }, true, true }, { { "--codesign", "-c" }, true, false }, - { { "--notarize", "-n" }, true, false } + { { "--notarize", "-n" }, true, false }, + { { "--env", "-E" }, true, true } }; // Insert the elements of runOptions into buildOptions @@ -5194,7 +5196,6 @@ int main (const int argc, const char* argv[]) { } if (platform.linux) { - compilerFlags += " -std=c++2a"; compilerFlags += " " + trim(exec("pkg-config --cflags gtk+-3.0 webkit2gtk-4.1").output); } From 97e98c58ee3737758d1cb2cd73a376bd4d67cf63 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Wed, 18 Oct 2023 15:13:00 -0400 Subject: [PATCH 239/256] fix(cli): typo with `permissions` setting access --- src/cli/cli.cc | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 734e41f313..8c0066052c 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -3180,24 +3180,24 @@ int main (const int argc, const char* argv[]) { manifestContext["android_manifest_xml_permissions"] = ""; - if (settings["permission_allow_notifications"] != "false") { + if (settings["permissions_allow_notifications"] != "false") { manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\" />\n"; } - if (settings["permission_allow_geolocation"] != "false") { + if (settings["permissions_allow_geolocation"] != "false") { manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" />\n"; manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" />\n"; - if (settings["permission_allow_geolocation_in_background"] != "false") { + if (settings["permissions_allow_geolocation_in_background"] != "false") { manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.ACCESS_BACKGROUND_LOCATION\" />\n"; } } - if (settings["permission_allow_user_media"] != "false") { - if (settings["permission_allow_camera"] != "false") { + if (settings["permissions_allow_user_media"] != "false") { + if (settings["permissions_allow_camera"] != "false") { manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.CAMERA\" />\n"; } - if (settings["permission_allow_microphone"] != "false") { + if (settings["permissions_allow_microphone"] != "false") { manifestContext["android_manifest_xml_permissions"] += "<uses-permission android:name=\"android.permission.CAPTURE_AUDIO_OUTPUT\" />\n"; } } @@ -4280,7 +4280,7 @@ int main (const int argc, const char* argv[]) { ); } - if (settings["permission_allow_user_media"] != "false") { + if (settings["permissions_allow_user_media"] != "false") { if (settings["permissions_allow_microphone"] != "false") { settings["ios_info_plist_data"] += ( " <string>microphone</string>\n" @@ -4568,15 +4568,15 @@ int main (const int argc, const char* argv[]) { ); } - if (settings["permission_allow_user_media"] != "false") { - if (settings["permission_allow_camera"] != "false") { + if (settings["permissions_allow_user_media"] != "false") { + if (settings["permissions_allow_camera"] != "false") { entitlementSettings["configured_entitlements"] += ( " <key>com.apple.security.device.camera</key>\n" " <true/>\n" ); } - if (settings["permission_allow_microphone"] != "false") { + if (settings["permissions_allow_microphone"] != "false") { entitlementSettings["configured_entitlements"] += ( " <key>com.apple.security.device.microphone</key>\n" " <true/>\n" @@ -5614,15 +5614,15 @@ int main (const int argc, const char* argv[]) { Map entitlementSettings; extendMap(entitlementSettings, settings); - if (settings["permission_allow_user_media"] != "false") { - if (settings["permission_allow_camera"] != "false") { + if (settings["permissions_allow_user_media"] != "false") { + if (settings["permissions_allow_camera"] != "false") { entitlementSettings["configured_entitlements"] += ( " <key>com.apple.security.device.camera</key>\n" " <true/>\n" ); } - if (settings["permission_allow_microphone"] != "false") { + if (settings["permissions_allow_microphone"] != "false") { entitlementSettings["configured_entitlements"] += ( " <key>com.apple.security.device.microphone</key>\n" " <true/>\n" From 0589593202ff36e3cf89b986cfc3b8bdc20430d7 Mon Sep 17 00:00:00 2001 From: Bret Comnes <bcomnes@gmail.com> Date: Wed, 18 Oct 2023 12:03:11 -0700 Subject: [PATCH 240/256] Add failing test --- test/src/path.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/src/path.js b/test/src/path.js index efea08b32f..dae9787af7 100644 --- a/test/src/path.js +++ b/test/src/path.js @@ -31,6 +31,7 @@ test('path.posix.join', (t) => { t.equal(path.posix.join('a', 'b', 'c'), 'a/b/c', 'join(a, b, c)') t.equal(path.posix.join('a', 'b', 'c', '../d'), 'a/b/d', 'join(a, b, c, ../d)') t.equal(path.posix.join('a', 'b', 'c', '../d', '../../b'), 'a/b', 'join(a, b, c, ../d, ../../b)') + t.equal(path.posix.join('/a', 'b', 'c'), '/a/b/c', 'join(/a, b, c)') }) /* From 5836121f6fb5aee7dffdcc9ad0259c9940c7fef0 Mon Sep 17 00:00:00 2001 From: Bret Comnes <bcomnes@gmail.com> Date: Wed, 18 Oct 2023 12:04:37 -0700 Subject: [PATCH 241/256] Implement root path fix --- api/path/path.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/api/path/path.js b/api/path/path.js index e4f3b2e13e..df218cc75a 100644 --- a/api/path/path.js +++ b/api/path/path.js @@ -153,9 +153,11 @@ export function relative (options, from, to) { export function join (options, ...components) { const { sep } = options const queries = [] - const joined = [] + const resolved = [] let protocol = null + const isAbsolute = components[0].trim().startsWith(sep) + while (components.length) { let component = String(components.shift() || '') const url = parseURL(component) || component @@ -175,20 +177,24 @@ export function join (options, ...components) { } for (const query of queries) { - if (query === '..' && joined.length > 1 && joined[0] !== '..') { - joined.pop() + if (query === '..' && resolved.length > 1 && resolved[0] !== '..') { + resolved.pop() } else if (query !== '.') { if (query.startsWith(sep)) { - joined.push(query.slice(1)) + resolved.push(query.slice(1)) } else if (query.endsWith(sep)) { - joined.push(query.slice(0, query.length - 1)) + resolved.push(query.slice(0, query.length - 1)) } else { - joined.push(query) + resolved.push(query) } } } - return joined.join(sep) + const joined = resolved.join(sep) + + return isAbsolute + ? sep + joined + : joined } /** From 11013c95ebc81944884ffb63c33652ba5cefb913 Mon Sep 17 00:00:00 2001 From: Bret Comnes <bcomnes@gmail.com> Date: Wed, 18 Oct 2023 12:21:15 -0700 Subject: [PATCH 242/256] Enable more tests and fix them --- api/path/path.js | 6 ++++-- test/src/path.js | 2 -- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/path/path.js b/api/path/path.js index df218cc75a..04c0add3c0 100644 --- a/api/path/path.js +++ b/api/path/path.js @@ -621,8 +621,10 @@ export class Path { i = this.pathname.lastIndexOf('\\') } - if (i === -1) return '' - const pathname = this.pathname.slice(i) + const pathname = (i > -1) + ? this.pathname.slice(i) + : this.pathname + i = pathname.lastIndexOf('.') if (i === -1) return '' return pathname.slice(i >= 0 ? i : undefined) diff --git a/test/src/path.js b/test/src/path.js index dae9787af7..6dac43a54d 100644 --- a/test/src/path.js +++ b/test/src/path.js @@ -34,7 +34,6 @@ test('path.posix.join', (t) => { t.equal(path.posix.join('/a', 'b', 'c'), '/a/b/c', 'join(/a, b, c)') }) -/* test('path.posix.dirname', (t) => { t.equal(path.posix.dirname('a/b/c'), 'a/b', 'a/b') t.equal(path.posix.dirname('a/b/c/d.js'), 'a/b/c', 'a/b/c') @@ -64,7 +63,6 @@ test('path.posix.extname', (t) => { t.equal(path.posix.extname('/a.js'), '.js', '.js') t.equal(path.posix.extname('a.js'), '.js', '.js') }) -*/ test('path.win32.resolve', (t) => { const cwd = '\\' From 2a8d5e33ee2bee322a0df77f423b63600ab9a48f Mon Sep 17 00:00:00 2001 From: Bret Comnes <bcomnes@gmail.com> Date: Wed, 18 Oct 2023 12:23:13 -0700 Subject: [PATCH 243/256] Enable more tests that were disabled --- api/README.md | 42 +++++++++++++++++++++--------------------- test/src/path.js | 2 -- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/api/README.md b/api/README.md index c3ef17e626..78b1c38e9c 100644 --- a/api/README.md +++ b/api/README.md @@ -1319,7 +1319,7 @@ Joins path components. This function may not return an absolute path. | :--- | :--- | :--- | | Not specified | string | | -## [`dirname(options, components)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L200) +## [`dirname(options, components)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L206) Computes directory name of path. @@ -1332,7 +1332,7 @@ Computes directory name of path. | :--- | :--- | :--- | | Not specified | string | | -## [`basename(options, components)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L236) +## [`basename(options, components)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L242) Computes base name of path. @@ -1345,7 +1345,7 @@ Computes base name of path. | :--- | :--- | :--- | | Not specified | string | | -## [`extname(options, path)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L248) +## [`extname(options, path)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L254) Computes extension name of path. @@ -1358,7 +1358,7 @@ Computes extension name of path. | :--- | :--- | :--- | | Not specified | string | | -## [`normalize(options, path)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L259) +## [`normalize(options, path)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L265) Computes normalized path @@ -1371,7 +1371,7 @@ Computes normalized path | :--- | :--- | :--- | | Not specified | string | | -## [`format(options, path)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L309) +## [`format(options, path)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L315) Formats `Path` object into a string. @@ -1384,7 +1384,7 @@ Formats `Path` object into a string. | :--- | :--- | :--- | | Not specified | string | | -## [`parse(path)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L325) +## [`parse(path)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L331) Parses input `path` into a `Path` instance. @@ -1396,11 +1396,11 @@ Parses input `path` into a `Path` instance. | :--- | :--- | :--- | | Not specified | object | | -## [Path](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L353) +## [Path](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L359) A container for a parsed Path. -### [`from(input, cwd)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L375) +### [`from(input, cwd)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L381) Creates a `Path` instance from `input` and optional `cwd`. @@ -1409,7 +1409,7 @@ Creates a `Path` instance from `input` and optional `cwd`. | input | PathComponent | | false | | | cwd | string | | true | | -### [`constructor(pathname, cwd)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L398) +### [`constructor(pathname, cwd)`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L404) `Path` class constructor. @@ -1418,47 +1418,47 @@ Creates a `Path` instance from `input` and optional `cwd`. | pathname | string | | false | | | cwd | string | Path.cwd() | true | | -### [`isRelative()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L467) +### [`isRelative()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L473) `true` if the path is relative, otherwise `false. -### [`value()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L474) +### [`value()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L480) The working value of this path. -### [`source()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L508) +### [`source()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L514) The original source, unresolved. -### [`parent()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L516) +### [`parent()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L522) Computed parent path. -### [`root()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L535) +### [`root()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L541) Computed root in path. -### [`dir()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L556) +### [`dir()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L562) Computed directory name in path. -### [`base()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L591) +### [`base()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L597) Computed base name in path. -### [`name()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L603) +### [`name()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L609) Computed base name in path without path extension. -### [`ext()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L611) +### [`ext()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L617) Computed extension name in path. -### [`drive()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L629) +### [`drive()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L637) The computed drive, if given in the path. -### [`toURL()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L636) +### [`toURL()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L644) @@ -1466,7 +1466,7 @@ The computed drive, if given in the path. | :--- | :--- | :--- | | Not specified | URL | | -### [`toString()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L644) +### [`toString()`](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L652) Converts this `Path` instance to a string. diff --git a/test/src/path.js b/test/src/path.js index 6dac43a54d..f993025181 100644 --- a/test/src/path.js +++ b/test/src/path.js @@ -80,7 +80,6 @@ test('path.win32.resolve', (t) => { t.equal(a___, cwd + 'a', 'path.win32.resolve() resolves path with 5 component') }) -/* test('path.win32.join', (t) => { t.equal(path.win32.join('a', 'b', 'c'), 'a\\b\\c', 'join(a, b, c)') t.equal(path.win32.join('a', 'b', 'c', '..\\d'), 'a\\b\\d', 'join(a, b, c, ..\\d)') @@ -262,4 +261,3 @@ test('path.relative', (t) => { t.equal(path.win32.relative('\\a\\b\\c', '\\a\\b\\c\\d'), 'd', 'd') t.equal(path.win32.relative('\\a\\b\\c', '\\a\\b\\c\\d\\e'), 'd\\e', 'd\\e') }) -*/ From 7d92fa0b223d7002baee6a9ae1c1caf7335e2f54 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Mon, 16 Oct 2023 19:30:24 -0400 Subject: [PATCH 244/256] fix(mac): avoid "This task has already been stopped" exceptions --- src/ipc/bridge.cc | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/ipc/bridge.cc b/src/ipc/bridge.cc index ac9f5bb9da..6e94831590 100644 --- a/src/ipc/bridge.cc +++ b/src/ipc/bridge.cc @@ -2333,7 +2333,18 @@ static void registerSchemeHandler (Router *router) { #if defined(__APPLE__) @implementation SSCIPCSchemeHandler -- (void) webView: (SSCBridgedWebView*) webview stopURLSchemeTask: (Task) task {} +{ + NSMutableSet<Task>* _tasks; +} +- (instancetype) init { + if (self = [super init]) { + _tasks = [NSMutableSet set]; + } + return self; +} +- (void) webView: (SSCBridgedWebView*) webview stopURLSchemeTask: (Task) task { + [_tasks removeObject: task]; +} - (void) webView: (SSCBridgedWebView*) webview startURLSchemeTask: (Task) task { static auto userConfig = SSC::getUserConfig(); static auto bundleIdentifier = userConfig["meta_bundle_identifier"]; @@ -2614,7 +2625,15 @@ static void registerSchemeHandler (Router *router) { body = (char *) data; } + auto tasks = _tasks; + [tasks addObject: task]; + auto invoked = self.router->invoke(message, body, bufsize, [=](Result result) { + // @TODO Communicate task cancellation to the route, so it can cancel its work. + if (![tasks containsObject: task]) { + return; + } + auto headers = [NSMutableDictionary dictionary]; headers[@"access-control-allow-origin"] = @"*"; headers[@"access-control-allow-methods"] = @"*"; @@ -2629,6 +2648,9 @@ static void registerSchemeHandler (Router *router) { if (result.post.event_stream != nullptr) { *result.post.event_stream = [task](const char* name, const char* data, bool finished) { + if (![tasks containsObject: task]) { + return false; + } auto event_name = [NSString stringWithUTF8String:name]; auto event_data = [NSString stringWithUTF8String:data]; if (event_name.length > 0 || event_data.length > 0) { @@ -2644,6 +2666,7 @@ static void registerSchemeHandler (Router *router) { } if (finished) { [task didFinish]; + [tasks removeObject:task]; } return true; }; @@ -2652,9 +2675,13 @@ static void registerSchemeHandler (Router *router) { } else if (result.post.chunk_stream != nullptr) { *result.post.chunk_stream = [task](const char* chunk, size_t chunk_size, bool finished) { + if (![tasks containsObject: task]) { + return false; + } [task didReceiveData:[NSData dataWithBytes:chunk length:chunk_size]]; if (finished) { [task didFinish]; + [tasks removeObject:task]; } return true; }; @@ -2687,6 +2714,7 @@ static void registerSchemeHandler (Router *router) { if (data != nullptr) { [task didReceiveData: data]; [task didFinish]; + [tasks removeObject:task]; } #if !__has_feature(objc_arc) From 0cc6a54a892dbfec81a831f5c129497b54356e86 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 17 Oct 2023 14:02:19 -0400 Subject: [PATCH 245/256] chore(fix): missing variable capture --- src/ipc/bridge.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ipc/bridge.cc b/src/ipc/bridge.cc index 6e94831590..06622c26bb 100644 --- a/src/ipc/bridge.cc +++ b/src/ipc/bridge.cc @@ -2646,7 +2646,7 @@ static void registerSchemeHandler (Router *router) { NSData* data = nullptr; if (result.post.event_stream != nullptr) { - *result.post.event_stream = [task](const char* name, const char* data, + *result.post.event_stream = [task, tasks](const char* name, const char* data, bool finished) { if (![tasks containsObject: task]) { return false; @@ -2673,7 +2673,7 @@ static void registerSchemeHandler (Router *router) { headers[@"content-type"] = @"text/event-stream"; headers[@"cache-control"] = @"no-store"; } else if (result.post.chunk_stream != nullptr) { - *result.post.chunk_stream = [task](const char* chunk, size_t chunk_size, + *result.post.chunk_stream = [task, tasks](const char* chunk, size_t chunk_size, bool finished) { if (![tasks containsObject: task]) { return false; From 645e65972e59eb60c73c76e8cdcb3bc6b1add43e Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Tue, 17 Oct 2023 18:47:14 -0400 Subject: [PATCH 246/256] chore(fix): use std::atomic boolean to avoid race condition --- src/ipc/bridge.cc | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/ipc/bridge.cc b/src/ipc/bridge.cc index 06622c26bb..49ce0402d5 100644 --- a/src/ipc/bridge.cc +++ b/src/ipc/bridge.cc @@ -1,4 +1,5 @@ #include <regex> +#include <unordered_map> #if defined(__APPLE__) #include <UniformTypeIdentifiers/UniformTypeIdentifiers.h> @@ -2334,16 +2335,17 @@ static void registerSchemeHandler (Router *router) { #if defined(__APPLE__) @implementation SSCIPCSchemeHandler { - NSMutableSet<Task>* _tasks; + // Map tasks to a block that prevents future didReceiveData/didFinish calls. + std::unordered_map<Task, std::function<void()>> _tasks; } -- (instancetype) init { - if (self = [super init]) { - _tasks = [NSMutableSet set]; - } - return self; +- (void) taskHasEnded: (Task) task { + auto taskEndedCallback = _tasks.at(task); + _tasks.erase(task); + + taskEndedCallback(); } - (void) webView: (SSCBridgedWebView*) webview stopURLSchemeTask: (Task) task { - [_tasks removeObject: task]; + [self taskHasEnded: task]; } - (void) webView: (SSCBridgedWebView*) webview startURLSchemeTask: (Task) task { static auto userConfig = SSC::getUserConfig(); @@ -2625,12 +2627,14 @@ static void registerSchemeHandler (Router *router) { body = (char *) data; } - auto tasks = _tasks; - [tasks addObject: task]; + std::atomic<bool> taskEnded = false; + _tasks.emplace(task, [&taskEnded]() { + taskEnded = true; + }); - auto invoked = self.router->invoke(message, body, bufsize, [=](Result result) { + auto invoked = self.router->invoke(message, body, bufsize, [=, &taskEnded](Result result) { // @TODO Communicate task cancellation to the route, so it can cancel its work. - if (![tasks containsObject: task]) { + if (taskEnded) { return; } @@ -2646,9 +2650,9 @@ static void registerSchemeHandler (Router *router) { NSData* data = nullptr; if (result.post.event_stream != nullptr) { - *result.post.event_stream = [task, tasks](const char* name, const char* data, + *result.post.event_stream = [self, task, &taskEnded](const char* name, const char* data, bool finished) { - if (![tasks containsObject: task]) { + if (taskEnded) { return false; } auto event_name = [NSString stringWithUTF8String:name]; @@ -2666,22 +2670,22 @@ static void registerSchemeHandler (Router *router) { } if (finished) { [task didFinish]; - [tasks removeObject:task]; + [self taskHasEnded:task]; } return true; }; headers[@"content-type"] = @"text/event-stream"; headers[@"cache-control"] = @"no-store"; } else if (result.post.chunk_stream != nullptr) { - *result.post.chunk_stream = [task, tasks](const char* chunk, size_t chunk_size, + *result.post.chunk_stream = [self, task, &taskEnded](const char* chunk, size_t chunk_size, bool finished) { - if (![tasks containsObject: task]) { + if (taskEnded) { return false; } [task didReceiveData:[NSData dataWithBytes:chunk length:chunk_size]]; if (finished) { [task didFinish]; - [tasks removeObject:task]; + [self taskHasEnded:task]; } return true; }; @@ -2714,7 +2718,7 @@ static void registerSchemeHandler (Router *router) { if (data != nullptr) { [task didReceiveData: data]; [task didFinish]; - [tasks removeObject:task]; + [self taskHasEnded:task]; } #if !__has_feature(objc_arc) From c13e40839472d238a095dce0090283170165a893 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Thu, 19 Oct 2023 18:15:32 -0400 Subject: [PATCH 247/256] chore(refactor): remove unnecessary `std::atomic` If an extension does work off the main thread, it will need to use `sapi_context_dispatch` to get back on the main thread before calling sapi_ipc_send_event or send_chunk. --- include/socket/extension.h | 4 ++++ src/ipc/bridge.cc | 16 +++++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/include/socket/extension.h b/include/socket/extension.h index 1c6392ed7a..5d34fef747 100644 --- a/include/socket/extension.h +++ b/include/socket/extension.h @@ -1148,6 +1148,8 @@ extern "C" { * * โš ๏ธ You must call `sapi_ipc_reply` before calling this function. * + * โš ๏ธ This must be called on the main thread (using `sapi_context_dispatch` if necessary). + * * Supported on iOS/macOS only. * * @param result - An IPC request result @@ -1174,6 +1176,8 @@ extern "C" { * โš ๏ธ The `name` and `data` arguments must be null-terminated strings. Either * can be empty as long as it's null-terminated and the other is not empty. * + * โš ๏ธ This must be called on the main thread (using `sapi_context_dispatch` if necessary). + * * Supported on iOS/macOS only. * * @param result - An IPC request result diff --git a/src/ipc/bridge.cc b/src/ipc/bridge.cc index 49ce0402d5..52e972a671 100644 --- a/src/ipc/bridge.cc +++ b/src/ipc/bridge.cc @@ -2627,12 +2627,12 @@ static void registerSchemeHandler (Router *router) { body = (char *) data; } - std::atomic<bool> taskEnded = false; + bool taskEnded = false; _tasks.emplace(task, [&taskEnded]() { taskEnded = true; }); - auto invoked = self.router->invoke(message, body, bufsize, [=, &taskEnded](Result result) { + auto invoked = self.router->invoke(message, body, bufsize, [=](Result result) { // @TODO Communicate task cancellation to the route, so it can cancel its work. if (taskEnded) { return; @@ -2650,8 +2650,9 @@ static void registerSchemeHandler (Router *router) { NSData* data = nullptr; if (result.post.event_stream != nullptr) { - *result.post.event_stream = [self, task, &taskEnded](const char* name, const char* data, - bool finished) { + *result.post.event_stream = [self, task, &taskEnded](const char* name, + const char* data, + bool finished) { if (taskEnded) { return false; } @@ -2661,7 +2662,7 @@ static void registerSchemeHandler (Router *router) { auto event = event_name.length > 0 && event_data.length > 0 ? [NSString stringWithFormat:@"event: %@\ndata: %@\n\n", - event_name, event_data] + event_name, event_data] : event_data.length > 0 ? [NSString stringWithFormat:@"data: %@\n\n", event_data] : [NSString stringWithFormat:@"event: %@\n\n", event_name]; @@ -2677,8 +2678,9 @@ static void registerSchemeHandler (Router *router) { headers[@"content-type"] = @"text/event-stream"; headers[@"cache-control"] = @"no-store"; } else if (result.post.chunk_stream != nullptr) { - *result.post.chunk_stream = [self, task, &taskEnded](const char* chunk, size_t chunk_size, - bool finished) { + *result.post.chunk_stream = [self, task, &taskEnded](const char* chunk, + size_t chunk_size, + bool finished) { if (taskEnded) { return false; } From 4e0c8cf144c3590a46b917d9ae8510f4afa943f6 Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Tue, 24 Oct 2023 21:54:43 +0200 Subject: [PATCH 248/256] docs(cli): add CLI.md --- api/CLI.md | 174 ++++++++++++++++++++++++++++++++++++++ bin/docs-generator/cli.js | 109 ++++++++++++++++++++++++ bin/generate-docs.js | 15 +++- src/cli/templates.hh | 4 + 4 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 api/CLI.md create mode 100644 bin/docs-generator/cli.js diff --git a/api/CLI.md b/api/CLI.md new file mode 100644 index 0000000000..b9cd7599ab --- /dev/null +++ b/api/CLI.md @@ -0,0 +1,174 @@ +# Command Line interface +These commands are available from the command line interface (CLI). +# ssc + +## Usage +```bash +ssc [SUBCOMMAND] [options] [<project-dir>] +ssc [SUBCOMMAND] -h +``` +## subcommands +| Option | Description | +| --- | --- | +| build | build project | +| list-devices | get the list of connected devices | +| init | create a new project (in the current directory) | +| install-app | install app to the device | +| print-build-dir | print build path to stdout | +| run | run application | +| env | print relavent environment variables | +| setup | install build dependencies | +## general options +| Option | Description | +| --- | --- | +| -h, --help | print help message | +| --prefix | print install path | +| -v, --version | print program version | +# ssc build +Build Socket application. +## Usage +```bash +ssc build [options] [<project-dir>] +``` +## options +| Option | Description | +| --- | --- | +| --platform=<platform> | platform target to build application for (defaults to host): +- android +- android-emulator +- ios +- ios-simulator | +| --port=<port> | load "index.html" from "http://localhost:<port>" | +| --test[=path] | indicate test mode, optionally importing a test file relative to resource files | +| --headless | build application to run in headless mode (without frame or window) | +| --prod | build for production (disables debugging info, inspector, etc.) | +| --stdin | read from stdin (dispacted as 'process.stdin' event to window #0) | +| -D, --debug | enable debug mode | +| -o, --only-build | only run build step, | +| -p, --package | package the app for distribution | +| -q, --quiet | hint for less log output | +| -r, --run | run after building | +| -V, --verbose | enable verbose output | +| -w, --watch | watch for changes to rerun build step | +## Linux options +| Option | Description | +| --- | --- | +| -f, --package-format=<format> | package a Linux application in a specified format for distribution: +- deb (default) +- zip | +## macOS options +| Option | Description | +| --- | --- | +| -c | code sign application with 'codesign' | +| -n | notarize application with 'notarytool' | +| -f, --package-format=<format> | package a macOS application in a specified format for distribution: +- zip (default) +- pkg | +## iOS options +| Option | Description | +| --- | --- | +| -c | code sign application during xcoddbuild +(requires '[ios] provisioning_profile' in 'socket.ini') | +## Windows options +| Option | Description | +| --- | --- | +| -f, --package-format=<format> | package a Windows application in a specified format for distribution: +- appx (default) | +# ssc list-devices +Get the list of connected devices. +## Usage +```bash +ssc list-devices [options] --platform=<platform> +``` +## options +| Option | Description | +| --- | --- | +| --platform=<platform> | platform target to list devices for: +- android +- ios | +| --ecid | show device ECID (ios only) | +| --udid | show device UDID (ios only) | +| --only | only show ECID or UDID of the first device (ios only) | +# ssc init +Create a new project. If the path is not provided, the new project will be created in the current directory. +## Usage +```bash +ssc init [<project-dir>] +``` +## options +| Option | Description | +| --- | --- | +| -C, --config | only create the config file | +| -n, --name | project name | +# ssc install-app +Install the app to the device or host target. +## Usage +```bash +ssc install-app [--platform=<platform>] [--device=<identifier>] [options] +``` +## options +| Option | Description | +| --- | --- | +| -D, --debug | enable debug output | +| --device[=identifier] | identifier (ecid, ID) of the device to install to +if not specified, tries to run on the current device | +| --platform=<platform> | platform to install application to device (defaults to host):: +- android +- ios | +| --prod | install production application | +| -V, --verbose | enable verbose output | +## macOS options +| Option | Description | +| --- | --- | +| --target=<target> | installation target for macOS application (defaults to '/') +the application is installed into '$target/Applications' | +# ssc print-build-dir +Create a new project (in the current directory) +## Usage +```bash +ssc print-build-dir [--platform=<platform>] [--prod] [--root] [<project-dir>] +``` +## options +| Option | Description | +| --- | --- | +| --platform | platform to print build directory for (defaults to host): +- android +- android-emulator +- ios +- ios-simulator | +| --prod | indicate production build directory | +| --root | print the root build directory | +# ssc run +Run application. +## Usage +```bash +ssc run [options] [<project-dir>] +``` +## options +| Option | Description | +| --- | --- | +| -D, --debug | enable debug mode | +| --headless | run application in headless mode (without frame or window) | +| --platform=<platform> | platform target to run application on (defaults to host): +- android +- android-emulator +- ios +- ios-simulator | +| --prod | build for production (disables debugging info, inspector, etc.) | +| --test[=path] | indicate test mode, optionally importing a test file relative to resource files | +| -V, --verbose | enable verbose output | +# ssc setup +Setup build tools for host or target platform. +Platforms not listed below can be setup using instructions at https://socketsupply.co/guides +## Usage +```bash +ssc setup [options] --platform=<platform> [-y|--yes] +``` +## options +| Option | Description | +| --- | --- | +| --platform=<platform> | platform target to run setup for (defaults to host): +- android +- ios | +| -q, --quiet | hint for less log output | +| -y, --yes | answer yes to any prompts | diff --git a/bin/docs-generator/cli.js b/bin/docs-generator/cli.js new file mode 100644 index 0000000000..7a1f86060a --- /dev/null +++ b/bin/docs-generator/cli.js @@ -0,0 +1,109 @@ +function toKebabCase (inputString) { + return inputString.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() +} + +function parseIni (iniText) { + const sections = {} + + const section = iniText.section === '' ? 'ssc' : `ssc ${iniText.section}` + + sections[section] = [] + + let currentSubSection + let description = [] + let option + + for (const line of iniText.extractedText.split('\n')) { + const trimmedLine = line.trim() + const isSubSection = /\w+:$/.test(trimmedLine) && trimmedLine[0] !== '-' + + // console.log('isSubsection', trimmedLine, isSubSection) + + if (trimmedLine.length === 0) { + // Skip empty lines. + continue + } + if (trimmedLine === 'ssc v{{ssc_version}}') { + // skip version + continue + } + if (line[0] != ' ' && !isSubSection) { + description.push(trimmedLine) + continue + } else { + if (isSubSection) { + // This line is a section heading. + currentSubSection = trimmedLine.replace(/:$/, ''); + sections[section][currentSubSection] = [] + } else { + if (!currentSubSection.includes('options') && currentSubSection !== 'subcommands') { + sections[section][currentSubSection].push(trimmedLine) + } else if (/.+\s{2,}.+/.test(trimmedLine)) { + const [value, description] = trimmedLine.split(/\s{2,}/) + option = value + sections[section][currentSubSection][value] = [description] + } else if (option) { + sections[section][currentSubSection][option].push(trimmedLine) + } + } + } + } + + sections[section].description = description + // console.log(sections[section]) + + return sections +} + +function generateIni (parts) { + return parts.map(parseIni) +} + +function createCliMd (sections) { + let md = '# Command Line interface\n' + md += 'These commands are available from the command line interface (CLI).\n' + + sections.map(section => { + Object.entries(section).map(([key, value]) => { + md += `# ${key}\n` + const { description, usage, ...options } = value + if (description) { + md += description.join('\n') + '\n' + } + if (usage) { + md += '## Usage\n' + md += '```bash\n' + md += usage.join('\n') + '\n' + md += '```\n' + } + Object.entries(options).map(([key, value]) => { + // create a table for each option + md += `## ${key}\n` + md += '| Option | Description |\n' + md += '| --- | --- |\n' + Object.entries(value).map(([key, value]) => { + md += `| ${key} | ${value.join('\n')} |\n` + }) + }) + }) + }) + return md +} + +// Generate CLI.md +export function generateCli (source) { + const startMarker = /constexpr auto gHelpText(\S*) = R"TEXT\(/gm + const endMarker = ')TEXT";' + + const sectionSources = [...source.matchAll(startMarker)].map(match => { + const startIndex = match.index + match[0].length + const remainingData = source.slice(startIndex) + const endIndex = remainingData.indexOf(endMarker) + const extractedText = remainingData.slice(0, endIndex) + return { section: toKebabCase(match[1]), extractedText } + }) + + const sections = generateIni(sectionSources) + return createCliMd(sections) +} + \ No newline at end of file diff --git a/bin/generate-docs.js b/bin/generate-docs.js index 2d702851b7..817ebf557f 100755 --- a/bin/generate-docs.js +++ b/bin/generate-docs.js @@ -4,6 +4,7 @@ import fs from 'node:fs/promises' import path from 'node:path' import { generateApiModuleDoc } from './docs-generator/api-module.js' import { generateConfig } from './docs-generator/config.js' +import { generateCli } from './docs-generator/cli.js' const VERSION = `v${(await fs.readFile('./VERSION.txt', 'utf8')).trim()}` const isCurrentTag = execSync('git describe --tags --always').toString().trim() === VERSION @@ -95,11 +96,17 @@ External docs: https://nodejs.org/api/events.html fs.writeFile(destFile, content) } +const templateFilePath = path.relative(process.cwd(), 'src/cli/templates.hh') +const templateFileSource = await fs.readFile(templateFilePath, 'utf8') + // socket/api/CONFIG.md { - const src = path.relative(process.cwd(), 'src/cli/templates.hh') - const source = await fs.readFile(src, 'utf8') - - const config = generateConfig(source) + const config = generateConfig(templateFileSource) fs.writeFile('api/CONFIG.md', config) } + +// socket/api/CLI.md +{ + const cli = generateCli(templateFileSource) + fs.writeFile('api/CLI.md', cli) +} diff --git a/src/cli/templates.hh b/src/cli/templates.hh index 4431d6e282..f7c875bb8b 100644 --- a/src/cli/templates.hh +++ b/src/cli/templates.hh @@ -27,6 +27,8 @@ general options: constexpr auto gHelpTextBuild = R"TEXT( ssc v{{ssc_version}} +Build Socket application. + usage: ssc build [options] [<project-dir>] @@ -144,6 +146,8 @@ options: constexpr auto gHelpTextRun = R"TEXT( ssc v{{ssc_version}} +Run application. + usage: ssc run [options] [<project-dir>] From e6ef00efe3ac79f4ec233f0270312e8049def149 Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Tue, 24 Oct 2023 23:28:47 +0200 Subject: [PATCH 249/256] docs(cli): fix styling and tables --- api/CLI.md | 50 ++++---------- bin/docs-generator/cli.js | 135 +++++++++++++++++++------------------- 2 files changed, 79 insertions(+), 106 deletions(-) diff --git a/api/CLI.md b/api/CLI.md index b9cd7599ab..d968c9f9fc 100644 --- a/api/CLI.md +++ b/api/CLI.md @@ -33,11 +33,7 @@ ssc build [options] [<project-dir>] ## options | Option | Description | | --- | --- | -| --platform=<platform> | platform target to build application for (defaults to host): -- android -- android-emulator -- ios -- ios-simulator | +| --platform=<platform> | platform target to build application for (defaults to host):<br>- android<br>- android-emulator<br>- ios<br>- ios-simulator | | --port=<port> | load "index.html" from "http://localhost:<port>" | | --test[=path] | indicate test mode, optionally importing a test file relative to resource files | | --headless | build application to run in headless mode (without frame or window) | @@ -53,27 +49,21 @@ ssc build [options] [<project-dir>] ## Linux options | Option | Description | | --- | --- | -| -f, --package-format=<format> | package a Linux application in a specified format for distribution: -- deb (default) -- zip | +| -f, --package-format=<format> | package a Linux application in a specified format for distribution:<br>- deb (default)<br>- zip | ## macOS options | Option | Description | | --- | --- | | -c | code sign application with 'codesign' | | -n | notarize application with 'notarytool' | -| -f, --package-format=<format> | package a macOS application in a specified format for distribution: -- zip (default) -- pkg | +| -f, --package-format=<format> | package a macOS application in a specified format for distribution:<br>- zip (default)<br>- pkg | ## iOS options | Option | Description | | --- | --- | -| -c | code sign application during xcoddbuild -(requires '[ios] provisioning_profile' in 'socket.ini') | +| -c | code sign application during xcoddbuild<br>(requires '[ios] provisioning_profile' in 'socket.ini') | ## Windows options | Option | Description | | --- | --- | -| -f, --package-format=<format> | package a Windows application in a specified format for distribution: -- appx (default) | +| -f, --package-format=<format> | package a Windows application in a specified format for distribution:<br>- appx (default) | # ssc list-devices Get the list of connected devices. ## Usage @@ -83,9 +73,7 @@ ssc list-devices [options] --platform=<platform> ## options | Option | Description | | --- | --- | -| --platform=<platform> | platform target to list devices for: -- android -- ios | +| --platform=<platform> | platform target to list devices for:<br>- android<br>- ios | | --ecid | show device ECID (ios only) | | --udid | show device UDID (ios only) | | --only | only show ECID or UDID of the first device (ios only) | @@ -110,18 +98,14 @@ ssc install-app [--platform=<platform>] [--device=<identifier>] [options] | Option | Description | | --- | --- | | -D, --debug | enable debug output | -| --device[=identifier] | identifier (ecid, ID) of the device to install to -if not specified, tries to run on the current device | -| --platform=<platform> | platform to install application to device (defaults to host):: -- android -- ios | +| --device[=identifier] | identifier (ecid, ID) of the device to install to<br>if not specified, tries to run on the current device | +| --platform=<platform> | platform to install application to device (defaults to host)::<br>- android<br>- ios | | --prod | install production application | | -V, --verbose | enable verbose output | ## macOS options | Option | Description | | --- | --- | -| --target=<target> | installation target for macOS application (defaults to '/') -the application is installed into '$target/Applications' | +| --target=<target> | installation target for macOS application (defaults to '/')<br>the application is installed into '$target/Applications' | # ssc print-build-dir Create a new project (in the current directory) ## Usage @@ -131,11 +115,7 @@ ssc print-build-dir [--platform=<platform>] [--prod] [--root] [<project-dir>] ## options | Option | Description | | --- | --- | -| --platform | platform to print build directory for (defaults to host): -- android -- android-emulator -- ios -- ios-simulator | +| --platform | platform to print build directory for (defaults to host):<br>- android<br>- android-emulator<br>- ios<br>- ios-simulator | | --prod | indicate production build directory | | --root | print the root build directory | # ssc run @@ -149,11 +129,7 @@ ssc run [options] [<project-dir>] | --- | --- | | -D, --debug | enable debug mode | | --headless | run application in headless mode (without frame or window) | -| --platform=<platform> | platform target to run application on (defaults to host): -- android -- android-emulator -- ios -- ios-simulator | +| --platform=<platform> | platform target to run application on (defaults to host):<br>- android<br>- android-emulator<br>- ios<br>- ios-simulator | | --prod | build for production (disables debugging info, inspector, etc.) | | --test[=path] | indicate test mode, optionally importing a test file relative to resource files | | -V, --verbose | enable verbose output | @@ -167,8 +143,6 @@ ssc setup [options] --platform=<platform> [-y|--yes] ## options | Option | Description | | --- | --- | -| --platform=<platform> | platform target to run setup for (defaults to host): -- android -- ios | +| --platform=<platform> | platform target to run setup for (defaults to host):<br>- android<br>- ios | | -q, --quiet | hint for less log output | | -y, --yes | answer yes to any prompts | diff --git a/bin/docs-generator/cli.js b/bin/docs-generator/cli.js index 7a1f86060a..d5a24af179 100644 --- a/bin/docs-generator/cli.js +++ b/bin/docs-generator/cli.js @@ -3,56 +3,56 @@ function toKebabCase (inputString) { } function parseIni (iniText) { - const sections = {} + const sections = {} - const section = iniText.section === '' ? 'ssc' : `ssc ${iniText.section}` + const section = iniText.section === '' ? 'ssc' : `ssc ${iniText.section}` - sections[section] = [] + sections[section] = [] - let currentSubSection - let description = [] - let option - - for (const line of iniText.extractedText.split('\n')) { - const trimmedLine = line.trim() - const isSubSection = /\w+:$/.test(trimmedLine) && trimmedLine[0] !== '-' + let currentSubSection + const description = [] + let option - // console.log('isSubsection', trimmedLine, isSubSection) + for (const line of iniText.extractedText.split('\n')) { + const trimmedLine = line.trim() + const isSubSection = /\w+:$/.test(trimmedLine) && trimmedLine[0] !== '-' - if (trimmedLine.length === 0) { - // Skip empty lines. - continue - } - if (trimmedLine === 'ssc v{{ssc_version}}') { - // skip version - continue - } - if (line[0] != ' ' && !isSubSection) { - description.push(trimmedLine) - continue - } else { - if (isSubSection) { - // This line is a section heading. - currentSubSection = trimmedLine.replace(/:$/, ''); - sections[section][currentSubSection] = [] - } else { - if (!currentSubSection.includes('options') && currentSubSection !== 'subcommands') { - sections[section][currentSubSection].push(trimmedLine) - } else if (/.+\s{2,}.+/.test(trimmedLine)) { - const [value, description] = trimmedLine.split(/\s{2,}/) - option = value - sections[section][currentSubSection][value] = [description] - } else if (option) { - sections[section][currentSubSection][option].push(trimmedLine) - } - } + // console.log('isSubsection', trimmedLine, isSubSection) + + if (trimmedLine.length === 0) { + // Skip empty lines. + continue + } + if (trimmedLine === 'ssc v{{ssc_version}}') { + // skip version + continue + } + if (line[0] !== ' ' && !isSubSection) { + description.push(trimmedLine) + continue + } else { + if (isSubSection) { + // This line is a section heading. + currentSubSection = trimmedLine.replace(/:$/, '') + sections[section][currentSubSection] = [] + } else { + if (!currentSubSection.includes('options') && currentSubSection !== 'subcommands') { + sections[section][currentSubSection].push(trimmedLine) + } else if (/.+\s{2,}.+/.test(trimmedLine)) { + const [value, description] = trimmedLine.split(/\s{2,}/) + option = value + sections[section][currentSubSection][value] = [description] + } else if (option) { + sections[section][currentSubSection][option].push(trimmedLine) } + } } - - sections[section].description = description - // console.log(sections[section]) + } + + sections[section].description = description + // console.log(sections[section]) - return sections + return sections } function generateIni (parts) { @@ -60,34 +60,34 @@ function generateIni (parts) { } function createCliMd (sections) { - let md = '# Command Line interface\n' - md += 'These commands are available from the command line interface (CLI).\n' + let md = '# Command Line interface\n' + md += 'These commands are available from the command line interface (CLI).\n' - sections.map(section => { - Object.entries(section).map(([key, value]) => { - md += `# ${key}\n` - const { description, usage, ...options } = value - if (description) { - md += description.join('\n') + '\n' - } - if (usage) { - md += '## Usage\n' - md += '```bash\n' - md += usage.join('\n') + '\n' - md += '```\n' - } - Object.entries(options).map(([key, value]) => { - // create a table for each option - md += `## ${key}\n` - md += '| Option | Description |\n' - md += '| --- | --- |\n' - Object.entries(value).map(([key, value]) => { - md += `| ${key} | ${value.join('\n')} |\n` - }) - }) + sections.forEach(section => { + Object.entries(section).forEach(([key, value]) => { + md += `# ${key}\n` + const { description, usage, ...options } = value + if (description) { + md += description.join('\n') + '\n' + } + if (usage) { + md += '## Usage\n' + md += '```bash\n' + md += usage.join('\n') + '\n' + md += '```\n' + } + Object.entries(options).forEach(([key, value]) => { + // create a table for each option + md += `## ${key}\n` + md += '| Option | Description |\n' + md += '| --- | --- |\n' + Object.entries(value).forEach(([key, value]) => { + md += `| ${key} | ${value.join('<br>')} |\n` }) + }) }) - return md + }) + return md } // Generate CLI.md @@ -106,4 +106,3 @@ export function generateCli (source) { const sections = generateIni(sectionSources) return createCliMd(sections) } - \ No newline at end of file From 5de50d8e74cf5774e91961c6cf674b861e0d350f Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Wed, 25 Oct 2023 16:11:48 +0200 Subject: [PATCH 250/256] docs(): add comment to autogenerated docs --- api/CLI.md | 94 +++++++++++++------ api/CONFIG.md | 3 + api/README.md | 45 +++++++++ bin/docs-generator/api-module.js | 3 +- bin/docs-generator/cli.js | 17 ++-- bin/docs-generator/config.js | 4 +- npm/packages/@socketsupply/socket-node/API.md | 3 + 7 files changed, 130 insertions(+), 39 deletions(-) diff --git a/api/CLI.md b/api/CLI.md index d968c9f9fc..f5a2c9cca8 100644 --- a/api/CLI.md +++ b/api/CLI.md @@ -1,13 +1,19 @@ +<!-- This file is generated by bin/docs-generator/cli.js --> +<!-- Do not edit this file directly. --> + # Command Line interface These commands are available from the command line interface (CLI). -# ssc -## Usage +## ssc + + +### Usage ```bash ssc [SUBCOMMAND] [options] [<project-dir>] ssc [SUBCOMMAND] -h ``` -## subcommands + +### subcommands | Option | Description | | --- | --- | | build | build project | @@ -18,19 +24,23 @@ ssc [SUBCOMMAND] -h | run | run application | | env | print relavent environment variables | | setup | install build dependencies | -## general options + +### general options | Option | Description | | --- | --- | | -h, --help | print help message | | --prefix | print install path | | -v, --version | print program version | -# ssc build + +## ssc build Build Socket application. -## Usage + +### Usage ```bash ssc build [options] [<project-dir>] ``` -## options + +### options | Option | Description | | --- | --- | | --platform=<platform> | platform target to build application for (defaults to host):<br>- android<br>- android-emulator<br>- ios<br>- ios-simulator | @@ -46,55 +56,68 @@ ssc build [options] [<project-dir>] | -r, --run | run after building | | -V, --verbose | enable verbose output | | -w, --watch | watch for changes to rerun build step | -## Linux options + +### Linux options | Option | Description | | --- | --- | | -f, --package-format=<format> | package a Linux application in a specified format for distribution:<br>- deb (default)<br>- zip | -## macOS options + +### macOS options | Option | Description | | --- | --- | | -c | code sign application with 'codesign' | | -n | notarize application with 'notarytool' | | -f, --package-format=<format> | package a macOS application in a specified format for distribution:<br>- zip (default)<br>- pkg | -## iOS options + +### iOS options | Option | Description | | --- | --- | | -c | code sign application during xcoddbuild<br>(requires '[ios] provisioning_profile' in 'socket.ini') | -## Windows options + +### Windows options | Option | Description | | --- | --- | | -f, --package-format=<format> | package a Windows application in a specified format for distribution:<br>- appx (default) | -# ssc list-devices + +## ssc list-devices Get the list of connected devices. -## Usage + +### Usage ```bash ssc list-devices [options] --platform=<platform> ``` -## options + +### options | Option | Description | | --- | --- | | --platform=<platform> | platform target to list devices for:<br>- android<br>- ios | | --ecid | show device ECID (ios only) | | --udid | show device UDID (ios only) | | --only | only show ECID or UDID of the first device (ios only) | -# ssc init + +## ssc init Create a new project. If the path is not provided, the new project will be created in the current directory. -## Usage + +### Usage ```bash ssc init [<project-dir>] ``` -## options + +### options | Option | Description | | --- | --- | | -C, --config | only create the config file | | -n, --name | project name | -# ssc install-app + +## ssc install-app Install the app to the device or host target. -## Usage + +### Usage ```bash ssc install-app [--platform=<platform>] [--device=<identifier>] [options] ``` -## options + +### options | Option | Description | | --- | --- | | -D, --debug | enable debug output | @@ -102,29 +125,36 @@ ssc install-app [--platform=<platform>] [--device=<identifier>] [options] | --platform=<platform> | platform to install application to device (defaults to host)::<br>- android<br>- ios | | --prod | install production application | | -V, --verbose | enable verbose output | -## macOS options + +### macOS options | Option | Description | | --- | --- | | --target=<target> | installation target for macOS application (defaults to '/')<br>the application is installed into '$target/Applications' | -# ssc print-build-dir + +## ssc print-build-dir Create a new project (in the current directory) -## Usage + +### Usage ```bash ssc print-build-dir [--platform=<platform>] [--prod] [--root] [<project-dir>] ``` -## options + +### options | Option | Description | | --- | --- | | --platform | platform to print build directory for (defaults to host):<br>- android<br>- android-emulator<br>- ios<br>- ios-simulator | | --prod | indicate production build directory | | --root | print the root build directory | -# ssc run + +## ssc run Run application. -## Usage + +### Usage ```bash ssc run [options] [<project-dir>] ``` -## options + +### options | Option | Description | | --- | --- | | -D, --debug | enable debug mode | @@ -133,16 +163,20 @@ ssc run [options] [<project-dir>] | --prod | build for production (disables debugging info, inspector, etc.) | | --test[=path] | indicate test mode, optionally importing a test file relative to resource files | | -V, --verbose | enable verbose output | -# ssc setup + +## ssc setup Setup build tools for host or target platform. Platforms not listed below can be setup using instructions at https://socketsupply.co/guides -## Usage + +### Usage ```bash ssc setup [options] --platform=<platform> [-y|--yes] ``` -## options + +### options | Option | Description | | --- | --- | | --platform=<platform> | platform target to run setup for (defaults to host):<br>- android<br>- ios | | -q, --quiet | hint for less log output | | -y, --yes | answer yes to any prompts | + diff --git a/api/CONFIG.md b/api/CONFIG.md index c13beafcc7..0894b0397a 100644 --- a/api/CONFIG.md +++ b/api/CONFIG.md @@ -1,3 +1,6 @@ +<!-- This file is generated by bin/docs-generator/config.js --> +<!-- Do not edit this file directly. --> + # Configuration basics The configuration file is a simple INI `socket.ini` file in the root of the project. diff --git a/api/README.md b/api/README.md index 78b1c38e9c..173acc8ea6 100644 --- a/api/README.md +++ b/api/README.md @@ -1,3 +1,6 @@ +<!-- This file is generated by bin/docs-generator/api-module.js --> +<!-- Do not edit this file directly. --> + # [Application](https://github.com/socketsupply/socket/blob/master/api/application.js#L13) @@ -230,6 +233,9 @@ The application's backend instance. | Not specified | Promise<ipc.Result> | | +<!-- This file is generated by bin/docs-generator/api-module.js --> +<!-- Do not edit this file directly. --> + # [Bluetooth](https://github.com/socketsupply/socket/blob/master/api/bluetooth.js#L12) @@ -305,6 +311,9 @@ Buffer module is a [third party](https://github.com/feross/buffer) vendor module External docs: https://nodejs.org/api/buffer.html +<!-- This file is generated by bin/docs-generator/api-module.js --> +<!-- Do not edit this file directly. --> + # [Crypto](https://github.com/socketsupply/socket/blob/master/api/crypto.js#L14) @@ -384,6 +393,9 @@ Generate `size` random bytes. | Not specified | Promise<Buffer> | A promise that resolves with an instance of socket.Buffer with the hash. | +<!-- This file is generated by bin/docs-generator/api-module.js --> +<!-- Do not edit this file directly. --> + # [DNS](https://github.com/socketsupply/socket/blob/master/api/dns/index.js#L17) @@ -432,6 +444,9 @@ Resolves a host name (e.g. `example.org`) into the first found A (IPv4) or | cb | function | | false | The function to call after the method is complete. | +<!-- This file is generated by bin/docs-generator/api-module.js --> +<!-- Do not edit this file directly. --> + # [DNS.promises](https://github.com/socketsupply/socket/blob/master/api/dns/promises.js#L17) @@ -463,6 +478,9 @@ External docs: https://nodejs.org/api/dns.html#dnspromiseslookuphostname-options | Not specified | Promise | | +<!-- This file is generated by bin/docs-generator/api-module.js --> +<!-- Do not edit this file directly. --> + # [Dgram](https://github.com/socketsupply/socket/blob/master/api/dgram.js#L13) @@ -709,6 +727,9 @@ Events module is a [third party](https://github.com/browserify/events/blob/main/ External docs: https://nodejs.org/api/events.html +<!-- This file is generated by bin/docs-generator/api-module.js --> +<!-- Do not edit this file directly. --> + # [FS](https://github.com/socketsupply/socket/blob/master/api/fs/index.js#L26) @@ -918,6 +939,9 @@ Asynchronously read all entries in a directory. | callback | function(Error?) | | false | | +<!-- This file is generated by bin/docs-generator/api-module.js --> +<!-- Do not edit this file directly. --> + # [FS.promises](https://github.com/socketsupply/socket/blob/master/api/fs/promises.js#L25) @@ -1077,6 +1101,9 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesw | Not specified | Promise<void> | | +<!-- This file is generated by bin/docs-generator/api-module.js --> +<!-- Do not edit this file directly. --> + # [IPC](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L33) @@ -1135,6 +1162,9 @@ Sends an async IPC command request with parameters. | Not specified | Promise<Result> | | +<!-- This file is generated by bin/docs-generator/api-module.js --> +<!-- Do not edit this file directly. --> + # [OS](https://github.com/socketsupply/socket/blob/master/api/os.js#L13) @@ -1250,6 +1280,9 @@ Returns the operating system name. | Not specified | string | The operating system name. | +<!-- This file is generated by bin/docs-generator/api-module.js --> +<!-- Do not edit this file directly. --> + # [Path](https://github.com/socketsupply/socket/blob/master/api/path/path.js#L9) @@ -1475,6 +1508,9 @@ Converts this `Path` instance to a string. | Not specified | string | | +<!-- This file is generated by bin/docs-generator/api-module.js --> +<!-- Do not edit this file directly. --> + # [Process](https://github.com/socketsupply/socket/blob/master/api/process.js#L9) @@ -1528,6 +1564,9 @@ Returns an object describing the memory usage of the Node.js process measured in | Not specified | Object | | +<!-- This file is generated by bin/docs-generator/api-module.js --> +<!-- Do not edit this file directly. --> + # [Test](https://github.com/socketsupply/socket/blob/master/api/test/index.js#L17) @@ -2076,6 +2115,12 @@ Retrieves the computed styles for a given element. | strict | boolean | | false | | +<!-- This file is generated by bin/docs-generator/api-module.js --> +<!-- Do not edit this file directly. --> + + +<!-- This file is generated by bin/docs-generator/api-module.js --> +<!-- Do not edit this file directly. --> # [Window](https://github.com/socketsupply/socket/blob/master/api/window.js#L11) diff --git a/bin/docs-generator/api-module.js b/bin/docs-generator/api-module.js index e081f4416c..863c9b284e 100644 --- a/bin/docs-generator/api-module.js +++ b/bin/docs-generator/api-module.js @@ -247,7 +247,8 @@ export function generateApiModuleDoc ({ const gitBase = 'https://github.com/socketsupply/socket/blob/' - let content = '' + let content = '<!-- This file is generated by bin/docs-generator/api-module.js -->\n' + content += '<!-- Do not edit this file directly. -->\n\n' for (const doc of docs) { let h = doc.export ? '##' : '###' diff --git a/bin/docs-generator/cli.js b/bin/docs-generator/cli.js index d5a24af179..c60ab02e0f 100644 --- a/bin/docs-generator/cli.js +++ b/bin/docs-generator/cli.js @@ -60,30 +60,33 @@ function generateIni (parts) { } function createCliMd (sections) { - let md = '# Command Line interface\n' - md += 'These commands are available from the command line interface (CLI).\n' + let md = '<!-- This file is generated by bin/docs-generator/cli.js -->\n' + md += '<!-- Do not edit this file directly. -->\n\n' + md += '# Command Line interface\n' + md += 'These commands are available from the command line interface (CLI).\n\n' sections.forEach(section => { Object.entries(section).forEach(([key, value]) => { - md += `# ${key}\n` + md += `## ${key}\n` const { description, usage, ...options } = value if (description) { - md += description.join('\n') + '\n' + md += description.join('\n') + '\n\n' } if (usage) { - md += '## Usage\n' + md += '### Usage\n' md += '```bash\n' md += usage.join('\n') + '\n' - md += '```\n' + md += '```\n\n' } Object.entries(options).forEach(([key, value]) => { // create a table for each option - md += `## ${key}\n` + md += `### ${key}\n` md += '| Option | Description |\n' md += '| --- | --- |\n' Object.entries(value).forEach(([key, value]) => { md += `| ${key} | ${value.join('<br>')} |\n` }) + md += '\n' }) }) }) diff --git a/bin/docs-generator/config.js b/bin/docs-generator/config.js index e5e75dbd8d..785f812906 100644 --- a/bin/docs-generator/config.js +++ b/bin/docs-generator/config.js @@ -35,7 +35,9 @@ function parseIni (iniText) { } function createConfigMd (sections) { - let md = '# Configuration basics\n' + let md = '<!-- This file is generated by bin/docs-generator/config.js -->\n' + md += '<!-- Do not edit this file directly. -->\n\n' + md += '# Configuration basics\n' md += ` The configuration file is a simple INI \`socket.ini\` file in the root of the project. The file is read on startup and the values are used to configure the project. diff --git a/npm/packages/@socketsupply/socket-node/API.md b/npm/packages/@socketsupply/socket-node/API.md index 45b2f53af3..c0ce671c0c 100644 --- a/npm/packages/@socketsupply/socket-node/API.md +++ b/npm/packages/@socketsupply/socket-node/API.md @@ -1,3 +1,6 @@ +<!-- This file is generated by bin/docs-generator/api-module.js --> +<!-- Do not edit this file directly. --> + ### [`send(options)`](https://github.com/socketsupply/socket/blob/master/npm/packages/@socketsupply/socket-node/index.js#L191) Send event to webview via IPC From b796d89353c20753bbe5cb84e77845815fca02cb Mon Sep 17 00:00:00 2001 From: heapwolf <paolo@socketsupply.co> Date: Thu, 26 Oct 2023 11:52:20 +0200 Subject: [PATCH 251/256] Update README.md --- README.md | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index e6cd095aa9..300599d353 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,43 @@ - -<p align="center"> - <a href="https://github.com/socketsupply/socket"><img src="https://user-images.githubusercontent.com/136109/230840267-7b7334b5-fee3-494b-aa4c-145e071f8471.png"/> -</p> +![image](https://github.com/socketsupply/socket/assets/136109/93abfcbe-e880-4548-b3e0-dc7e09292ca6) ### Description -Web Developers use `Socket Runtime` to create apps for any OS, desktop, or mobile. You can use plain old HTML, CSS, and JavaScript, as well as your favorite front-end libraries for example React, Svelte, and Vue. - -`Socket Runtime` exposes primitives needed for building peer-to-peer and local-first applications, such as Bluetooth, UDP, and robust file system access. Our P2P component can help you connect your app's users, and let them communicate directly, without the cloud or any servers at all. +Web Developers use `Socket runtime` to create apps for any OS, desktop, and mobile. You can use plain old HTML, CSS, and JavaScript, as well as your favorite front-end libraries like Next.js, React, Svelte, or Vue. -`The Socket Runtime CLI` compiles applications into hybrid-native applications &mbasp; meaning, a combination of web code running in a platform's "WebView" along with platform-native code: Kotlin/Java on Android, Swift/Objective-C on iOS, C++ on Windows or Linux, etc. +The `Socket runtime CLI` outputs hybrid native-web apps that combine your code with the runtime. Your code is rendered using the OS's native "WebView" component. Platform features are implemented natively and made available to the JavaScript environment in a way that is secure and fully sandboxed on every platform. Native APIs like Bluetooth and UDP make local-first and peer-to-peer software design patterns as first class considerations. -### ๐Ÿ’ก Features +### Features -* Local First, a full-featured File system API & Bluetooth. -* P2P & Cloud, built to support a new generation of apps that can connect directly to each other by providing a high-performance UDP API. -* Use any backend, business logic can be written in any language, Python, Rust, Node.js, etc. The backend is even completely optional. -* Use any frontend, you can use your favorite frontend framework to create your UIs: React, Svelte, Vue, and more. -* Maintainable, zero dependencies, and a smaller code base than any other competing project. -* Lean & Fast, uses a smaller memory footprint and creates smaller binaries than any other competing project. +* Any backend — Business logic can be written in any language, Python, Rust, Node.js, etc. The backend is even completely optional. +* Any frontend — Use your favorite frontend framework to create your UIs: React, Svelte, Vue, and more. +* Local-first — A full-featured, familiar File system API, native add-ons and full cross platform support for Bluetooth. +* Not just Cloud — P2P helps you reliably move work out of the cloud, beyond the edge, and onto the devices that can communicate directly with each other. +* Maintainable — Socket runtime has Zero external dependencies, and a smaller code base than any other competing project. +* Lean & Fast — Socket runtime has a smaller memory footprint and creates smaller binaries than any other competing project. -### ๐Ÿ”‘ FAQ +### FAQ -Check the FAQs on our [Website](https://socketsupply.co/) to learn more. +Check the FAQs on our [Website](https://socketsupply.co/guides/#faq) to learn more. -### ๐Ÿงฑ Building your first Socket app! +### Building your first Socket app! `Create Socket App` is similar to React's `Create React App`, we provide a few basic boilerplates and some strong opinions so you can get coding on a production-quality app as quickly as possible. Please check [create-socket-app Repo](https://github.com/socketsupply/create-socket-app) to get started and to learn more. You can also check our `Examples` in the [Examples Repo](https://github.com/socketsupply/socket-examples). -### ๐Ÿ“š Documentation +### Documentation The full documentation can be found on the [Socket Runtime](https://socketsupply.co/) website. The `Socket Runtime` documentation covers Socket APIs, includes examples, multiple guides (`Apple`, `Desktop`, and `Mobile`), `P2P` documentation, and more. -### ๐Ÿงช Testing +### Testing `Socket` provides a built-in `test runner` similar to `node:test` which outputs the test results in [TAP](https://testanything.org/) format. You can also check [`test/`](test/) for the unit and integration test suite. -### ๐Ÿ™ Contributing +### Contributing We welcome contributions from everyone! Please check our [Contribution Guide](CONTRIBUTING.md) to learn more. Don't hesitate to stop by [Discord](https://discord.com/invite/YPV32gKCsH) and ask the team about your issue and if someone is already working on it. From df3260913e28335398ba8a53fb2d433f78a45b0d Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Thu, 26 Oct 2023 09:24:47 -0400 Subject: [PATCH 252/256] fix(src/ipc/bridge.cc): add missing ios branch for base path in router resolution --- src/ipc/bridge.cc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ipc/bridge.cc b/src/ipc/bridge.cc index 52e972a671..3ab61d7afe 100644 --- a/src/ipc/bridge.cc +++ b/src/ipc/bridge.cc @@ -2392,7 +2392,11 @@ static void registerSchemeHandler (Router *router) { NSData* data = nullptr; bool isModule = false; - auto basePath = String(NSBundle.mainBundle.resourcePath.UTF8String); + #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR + const auto basePath = String(NSBundle.mainBundle.resourcePath.UTF8String) + "/ui"; + #else + const auto basePath = String(NSBundle.mainBundle.resourcePath.UTF8String); + #endif auto path = String(components.path.UTF8String); auto ext = String( From 13c80a83b8005f3c442bb852f248e7c95ccd676b Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Thu, 26 Oct 2023 17:17:35 +0200 Subject: [PATCH 253/256] refactor(src/cli/cli.cc): better check for available iOS Simulator devices --- src/cli/cli.cc | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/cli/cli.cc b/src/cli/cli.cc index 8c0066052c..aba20f571e 100644 --- a/src/cli/cli.cc +++ b/src/cli/cli.cc @@ -852,6 +852,22 @@ void signalHandler (int signal) { appMutex.unlock(); } +void checkIosSimulatorDeviceAvailability (const String& device) { + if (device.size() == 0) { + log("ERROR: [ios] simulator_device option is empty"); + exit(1); + } + auto const rDevices = exec("xcrun simctl list devices available | grep -e \" \""); + auto isDeviceFound = rDevices.output.find(device) != String::npos; + + if (!isDeviceFound) { + log("ERROR: [ios] simulator_device option is invalid: " + device); + log("available devices:\n" + rDevices.output); + log("please update your socket.ini with a valid device or install Simulator runtime (https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes)"); + exit(1); + } +} + int runApp (const Path& path, const String& args, bool headless) { auto cmd = path.string(); @@ -1007,10 +1023,8 @@ int runApp (const Path& path, const String& args) { void runIOSSimulator (const Path& path, Map& settings) { #ifndef _WIN32 - if (settings["ios_simulator_device"].size() == 0) { - log("ERROR: [ios] simulator_device option is empty"); - exit(1); - } + checkIosSimulatorDeviceAvailability(settings["ios_simulator_device"]); + String deviceType; StringStream listDeviceTypesCommand; listDeviceTypesCommand @@ -4522,14 +4536,10 @@ int main (const int argc, const char* argv[]) { } if (flagBuildForIOS) { - if (flagBuildForSimulator && settings["ios_simulator_device"].size() == 0) { - log("ERROR: [ios] simulator_device option is empty"); - exit(1); - } - if (flagBuildForSimulator) { + checkIosSimulatorDeviceAvailability(settings["ios_simulator_device"]); log("building for iOS Simulator"); - } else { + } else { log("building for iOS"); } @@ -4667,14 +4677,6 @@ int main (const int argc, const char* argv[]) { auto rArchive = exec(archiveCommand.str().c_str()); if (rArchive.exitCode != 0) { - auto const noDevice = rArchive.output.find("The requested device could not be found because no available devices matched the request."); - if (noDevice != String::npos) { - log("ERROR: [ios] simulator_device " + settings["ios_simulator_device"] + " from your socket.ini was not found"); - auto const rDevices = exec("xcrun simctl list devices available | grep -e \" \""); - log("available devices:\n" + rDevices.output); - log("please update your socket.ini with a valid device or install Simulator runtime (https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes)"); - exit(1); - } log("ERROR: failed to archive project"); log(rArchive.output); fs::current_path(oldCwd); From 7e2b6872d965b5bb4e01736c79fc9448cc486269 Mon Sep 17 00:00:00 2001 From: Sergey Rubanov <chi187@gmail.com> Date: Thu, 26 Oct 2023 14:23:25 +0200 Subject: [PATCH 254/256] docs(ipc,window): clarifications and spelling fixes --- api/README.md | 72 +++++++++++++++++++++++++++----------------------- api/index.d.ts | 4 ++- api/ipc.js | 4 +++ api/window.js | 9 ++++--- 4 files changed, 52 insertions(+), 37 deletions(-) diff --git a/api/README.md b/api/README.md index 173acc8ea6..75f73e22e3 100644 --- a/api/README.md +++ b/api/README.md @@ -1104,12 +1104,16 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesw <!-- This file is generated by bin/docs-generator/api-module.js --> <!-- Do not edit this file directly. --> -# [IPC](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L33) +# [IPC](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L37) This is a low-level API that you don't need unless you are implementing a library on top of Socket SDK. A Socket SDK app has two or three processes. + When you need to send a message to another window or to the backend, you + should use the `application` module to get a reference to the window and + use the `send` method to send a message. + - The `Render` process, is the UI where the HTML, CSS, and JS are run. - The `Bridge` process, is the thin layer of code that manages everything. - The `Main` process, is for apps that need to run heavier compute jobs. And @@ -1134,7 +1138,7 @@ External docs: https://nodejs.org/dist/latest-v20.x/docs/api/fs.html#fspromisesw import { send } from 'socket:ipc' ``` -## [`emit(name, value, target, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1087) +## [`emit(name, value, target, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1091) Emit event to be dispatched on `window` object. @@ -1145,7 +1149,7 @@ Emit event to be dispatched on `window` object. | target | EventTarget | window | true | | | options | Object | | true | | -## [`send(command, value, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1146) +## [`send(command, value, options)`](https://github.com/socketsupply/socket/blob/master/api/ipc.js#L1150) Sends an async IPC command request with parameters. @@ -2122,20 +2126,20 @@ Retrieves the computed styles for a given element. <!-- This file is generated by bin/docs-generator/api-module.js --> <!-- Do not edit this file directly. --> -# [Window](https://github.com/socketsupply/socket/blob/master/api/window.js#L11) +# [Window](https://github.com/socketsupply/socket/blob/master/api/window.js#L12) -External docs: module:Application Application Provides ApplicationWindow class and methods - Usaully you don't need to use this module directly, instance of ApplicationWindow - are returned by the methods of the {@link module:Application Application} module. + Don't use this module directly, get instances of ApplicationWindow with + `socket:application` methods like `getCurrentWindow`, `createWindow`, + `getWindow`, and `getWindows`. -## [ApplicationWindow](https://github.com/socketsupply/socket/blob/master/api/window.js#L31) +## [ApplicationWindow](https://github.com/socketsupply/socket/blob/master/api/window.js#L32) Represents a window in the application -### [`index()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L59) +### [`index()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L60) Get the index of the window @@ -2143,7 +2147,7 @@ Get the index of the window | :--- | :--- | :--- | | Not specified | number | the index of the window | -### [`getSize()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L67) +### [`getSize()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L68) Get the size of the window @@ -2151,7 +2155,7 @@ Get the size of the window | :--- | :--- | :--- | | Not specified | { width: number, height: number | } - the size of the window | -### [`getTitle()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L78) +### [`getTitle()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L79) Get the title of the window @@ -2159,7 +2163,7 @@ Get the title of the window | :--- | :--- | :--- | | Not specified | string | the title of the window | -### [`getStatus()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L86) +### [`getStatus()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L87) Get the status of the window @@ -2167,7 +2171,7 @@ Get the status of the window | :--- | :--- | :--- | | Not specified | string | the status of the window | -### [`close()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L94) +### [`close()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L95) Close the window @@ -2175,7 +2179,7 @@ Close the window | :--- | :--- | :--- | | Not specified | Promise<object> | the options of the window | -### [`show()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L109) +### [`show()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L110) Shows the window @@ -2183,7 +2187,7 @@ Shows the window | :--- | :--- | :--- | | Not specified | Promise<ipc.Result> | | -### [`hide()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L118) +### [`hide()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L119) Hides the window @@ -2191,7 +2195,7 @@ Hides the window | :--- | :--- | :--- | | Not specified | Promise<ipc.Result> | | -### [`setTitle(title)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L128) +### [`setTitle(title)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L129) Sets the title of the window @@ -2203,7 +2207,7 @@ Sets the title of the window | :--- | :--- | :--- | | Not specified | Promise<ipc.Result> | | -### [`setSize(opts)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L141) +### [`setSize(opts)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L142) Sets the size of the window @@ -2217,7 +2221,7 @@ Sets the size of the window | :--- | :--- | :--- | | Not specified | Promise<ipc.Result> | | -### [`navigate(path)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L181) +### [`navigate(path)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L182) Navigate the window to a given path @@ -2229,7 +2233,7 @@ Navigate the window to a given path | :--- | :--- | :--- | | Not specified | Promise<ipc.Result> | | -### [`showInspector()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L190) +### [`showInspector()`](https://github.com/socketsupply/socket/blob/master/api/window.js#L191) Opens the Web Inspector for the window @@ -2237,7 +2241,7 @@ Opens the Web Inspector for the window | :--- | :--- | :--- | | Not specified | Promise<object> | | -### [`setBackgroundColor(opts)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L207) +### [`setBackgroundColor(opts)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L208) Sets the background color of the window @@ -2253,7 +2257,7 @@ Sets the background color of the window | :--- | :--- | :--- | | Not specified | Promise<object> | | -### [`setContextMenu(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L217) +### [`setContextMenu(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L218) Opens a native context menu. @@ -2265,7 +2269,7 @@ Opens a native context menu. | :--- | :--- | :--- | | Not specified | Promise<object> | | -### [`showOpenFilePicker(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L234) +### [`showOpenFilePicker(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L235) Shows a native open file dialog. @@ -2277,7 +2281,7 @@ Shows a native open file dialog. | :--- | :--- | :--- | | Not specified | Promise<string[]> | an array of file paths | -### [`showSaveFilePicker(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L245) +### [`showSaveFilePicker(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L246) Shows a native save file dialog. @@ -2289,7 +2293,7 @@ Shows a native save file dialog. | :--- | :--- | :--- | | Not specified | Promise<string[]> | an array of file paths | -### [`showDirectoryFilePicker(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L256) +### [`showDirectoryFilePicker(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L257) Shows a native directory dialog. @@ -2301,9 +2305,11 @@ Shows a native directory dialog. | :--- | :--- | :--- | | Not specified | Promise<string[]> | an array of file paths | -### [`send(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L271) +### [`send(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L274) + +This is a high-level API that you should use instead of `ipc.send` when + you want to send a message to another window or to the backend. -Sends an IPC message to the window or to qthe backend. | Argument | Type | Default | Optional | Description | | :--- | :--- | :---: | :---: | :--- | @@ -2313,7 +2319,7 @@ Sends an IPC message to the window or to qthe backend. | options.event | string | | false | the event to send | | options.value | string \| object | | true | the value to send | -### [`openExternal(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L308) +### [`openExternal(options)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L311) Opens an URL in the default browser. @@ -2325,7 +2331,7 @@ Opens an URL in the default browser. | :--- | :--- | :--- | | Not specified | Promise<ipc.Result> | | -### [`addListener(event, cb)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L319) +### [`addListener(event, cb)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L322) Adds a listener to the window. @@ -2334,7 +2340,7 @@ Adds a listener to the window. | event | string | | false | the event to listen to | | cb | function(*): void | | false | the callback to call | -### [`on(event, cb)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L337) +### [`on(event, cb)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L340) Adds a listener to the window. An alias for `addListener`. @@ -2343,7 +2349,7 @@ Adds a listener to the window. An alias for `addListener`. | event | string | | false | the event to listen to | | cb | function(*): void | | false | the callback to call | -### [`once(event, cb)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L354) +### [`once(event, cb)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L357) Adds a listener to the window. The listener is removed after the first call. @@ -2352,7 +2358,7 @@ Adds a listener to the window. The listener is removed after the first call. | event | string | | false | the event to listen to | | cb | function(*): void | | false | the callback to call | -### [`removeListener(event, cb)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L370) +### [`removeListener(event, cb)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L373) Removes a listener from the window. @@ -2361,7 +2367,7 @@ Removes a listener from the window. | event | string | | false | the event to remove the listener from | | cb | function(*): void | | false | the callback to remove | -### [`removeAllListeners(event)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L383) +### [`removeAllListeners(event)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L386) Removes all listeners from the window. @@ -2369,7 +2375,7 @@ Removes all listeners from the window. | :--- | :--- | :---: | :---: | :--- | | event | string | | false | the event to remove the listeners from | -### [`off(event, cb)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L399) +### [`off(event, cb)`](https://github.com/socketsupply/socket/blob/master/api/window.js#L402) Removes a listener from the window. An alias for `removeListener`. diff --git a/api/index.d.ts b/api/index.d.ts index 9eb5f1d9ff..13c8ca08ab 100644 --- a/api/index.d.ts +++ b/api/index.d.ts @@ -3289,7 +3289,9 @@ declare module "socket:window" { */ showDirectoryFilePicker(options: object): Promise<string[]>; /** - * Sends an IPC message to the window or to qthe backend. + * This is a high-level API that you should use instead of `ipc.send` when + * you want to send a message to another window or to the backend. + * * @param {object} options - an options object * @param {number=} options.window - the window to send the message to * @param {boolean=} [options.backend = false] - whether to send the message to the backend diff --git a/api/ipc.js b/api/ipc.js index 4f381d3a94..d112b71b38 100644 --- a/api/ipc.js +++ b/api/ipc.js @@ -4,6 +4,10 @@ * This is a low-level API that you don't need unless you are implementing * a library on top of Socket SDK. A Socket SDK app has two or three processes. * + * When you need to send a message to another window or to the backend, you + * should use the `application` module to get a reference to the window and + * use the `send` method to send a message. + * * - The `Render` process, is the UI where the HTML, CSS, and JS are run. * - The `Bridge` process, is the thin layer of code that manages everything. * - The `Main` process, is for apps that need to run heavier compute jobs. And diff --git a/api/window.js b/api/window.js index d0da6cb888..618c1f7d90 100644 --- a/api/window.js +++ b/api/window.js @@ -4,8 +4,9 @@ * * Provides ApplicationWindow class and methods * - * Usaully you don't need to use this module directly, instance of ApplicationWindow - * are returned by the methods of the {@link module:Application Application} module. + * Don't use this module directly, get instances of ApplicationWindow with + * `socket:application` methods like `getCurrentWindow`, `createWindow`, + * `getWindow`, and `getWindows`. */ import { isValidPercentageValue } from './util.js' @@ -260,7 +261,9 @@ export class ApplicationWindow { } /** - * Sends an IPC message to the window or to qthe backend. + * This is a high-level API that you should use instead of `ipc.send` when + * you want to send a message to another window or to the backend. + * * @param {object} options - an options object * @param {number=} options.window - the window to send the message to * @param {boolean=} [options.backend = false] - whether to send the message to the backend From d015588b062af4831a0c1eca0c82b985145974ab Mon Sep 17 00:00:00 2001 From: Joseph Werle <joseph.werle@gmail.com> Date: Fri, 27 Oct 2023 16:13:29 -0400 Subject: [PATCH 255/256] fix(src/ipc/bridge.cc): check if task is in map first --- src/ipc/bridge.cc | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/ipc/bridge.cc b/src/ipc/bridge.cc index 3ab61d7afe..6e3119e85b 100644 --- a/src/ipc/bridge.cc +++ b/src/ipc/bridge.cc @@ -2339,10 +2339,13 @@ static void registerSchemeHandler (Router *router) { std::unordered_map<Task, std::function<void()>> _tasks; } - (void) taskHasEnded: (Task) task { - auto taskEndedCallback = _tasks.at(task); - _tasks.erase(task); - - taskEndedCallback(); + if (task != nullptr && _tasks.contains(task)) { + auto taskEndedCallback = _tasks.at(task); + _tasks.erase(task); + if (taskEndedCallback != nullptr) { + taskEndedCallback(); + } + } } - (void) webView: (SSCBridgedWebView*) webview stopURLSchemeTask: (Task) task { [self taskHasEnded: task]; From 8ffb16603cc11e4a389dd5d0115c8a225f61d09c Mon Sep 17 00:00:00 2001 From: heapwolf <paolo@socketsupply.co> Date: Sat, 28 Oct 2023 16:04:55 +0200 Subject: [PATCH 256/256] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 300599d353..17200750c8 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ The `Socket runtime CLI` outputs hybrid native-web apps that combine your code w * Any backend — Business logic can be written in any language, Python, Rust, Node.js, etc. The backend is even completely optional. * Any frontend — Use your favorite frontend framework to create your UIs: React, Svelte, Vue, and more. +* Batteries Included — Native Add-ons are supported, but we ship everything you need for the majority of use cases. * Local-first — A full-featured, familiar File system API, native add-ons and full cross platform support for Bluetooth. * Not just Cloud — P2P helps you reliably move work out of the cloud, beyond the edge, and onto the devices that can communicate directly with each other. * Maintainable — Socket runtime has Zero external dependencies, and a smaller code base than any other competing project.